contract MoneyStampsERC721 is ERC721Metadata, ERC721Enumerable {
    using SafeMath for uint256;

    /***********************************|
    |        Variables and Events       |
    |__________________________________*/

    IERC20 internal dai;
    IChai internal chai;

    mapping(uint256 => uint256) internal chaiBalanceByTokenId; // Amount of Chai minted from Dai deposited

    uint256 internal totalMintedTokens;
    uint256 internal mintFee;
    uint256 internal collectedFees;
    uint256 internal requiredFunding; // Amount of Dai to deposit when minting

    address private owner; // To be assigned to a DAO

    bytes16 public version = "v0.0.1";

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /***********************************|
    |             Modifiers             |
    |__________________________________*/

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        require(msg.sender == owner, "E102");
        _;
    }

    /***********************************|
    |          Initialization           |
    |__________________________________*/

    constructor() ERC721Metadata("$TAMP", "$TAMP") public {
        // requiredFunding = 1e18;
        // mintFee = 50;    //  0.5% of Chai from deposited Dai

        owner = msg.sender;
        emit OwnershipTransferred(address(0), owner);
    }

    /***********************************|
    |            Public Read            |
    |__________________________________*/

    /**
     * @notice Gets the address of the contract owner.
     * @return The address of the owner
     */
    function getContractOwner() public view returns (address) {
        return owner;
    }

    /***********************************|
    |            Stamp Glue             |
    |__________________________________*/

    /**
     * @notice Gets the Amount of Base DAI held in the Token (amount token was minted with)
     * @return The Amount of DAI held in the Token
     */
    function baseStampWeight() public view returns (uint256) {
        return requiredFunding;
    }

    /**
     * @notice Gets the amount of interest the Token has generated (its accumulated stamp-loot)
     * @param _tokenId      The ID of the Token
     * @return  The amount of interest the Token has generated
     */
    function currentStampLoot(uint256 _tokenId) public returns (uint256) {
        require(_exists(_tokenId), "E402");

        uint256 currentLoot = chai.dai(chaiBalanceByTokenId[_tokenId]);
        if (requiredFunding >= currentLoot) { return 0; }
        return currentLoot.sub(requiredFunding);
    }

    /**
     * @notice Allows the owner of the Token to collect the interest generated from the token
     *  without removing the underlying DAI that is held in the token
     * @param _tokenId      The ID of the Token
     * @return The amount of interest released from the token
     */
    function peelStamp(uint256 _tokenId) public returns (uint256) {
        require(_isApprovedOrOwner(msg.sender, _tokenId), "E103");

        uint256 _currentLootInDai = currentStampLoot(_tokenId);
        require(_currentLootInDai > 0, "E403");

        uint256 _paidChai = _payoutStampedDai(msg.sender, _currentLootInDai);
        chaiBalanceByTokenId[_tokenId] = chaiBalanceByTokenId[_tokenId].sub(_paidChai);

        return _currentLootInDai;
    }

    /***********************************|
    |            Public Mint            |
    |__________________________________*/

    /**
     * @notice Mints multiple new Stamps
     * @param _to       The owner address to assign the new tokens to
     * @param _amount   The amount of tokens to mint
     * @param _stamp     Custom lexical stamp used to establish authenticity 
     * @return  The IDs of the newly minted tokens
     */
    function mintStamps(address _to, uint256 _amount, string memory _stamp) public returns (uint256[] memory) {
        address _self = address(this);
        uint256 i;
        uint256 _tokenId;
        uint256 _totalDai;
        uint256[] memory _tokenIds = new uint256[](_amount);

        for (i = 0; i < _amount; ++i) {
            _totalDai = requiredFunding.add(_totalDai);

            _tokenId = (totalMintedTokens.add(i+1));
            _tokenIds[i] = _tokenId;
            _mint(_to, _tokenId);
    	    _setTokenURI(_tokenId, _stamp);
        }
        totalMintedTokens = totalMintedTokens.add(_amount);

        if (_totalDai > 0) {
            // Transfer DAI from User to Contract
            _collectRequiredDai(msg.sender, _totalDai);

            uint256 _balance = chai.balanceOf(_self);
            for (i = 0; i < _amount; ++i) {
                _tokenId = _tokenIds[i];

                // Tokenize Interest
                chai.join(_self, requiredFunding);

                // Track Chai in each Token
                 chaiBalanceByTokenId[_tokenId] = _totalChaiForToken(chai.balanceOf(_self).sub(_balance));
                _balance = chai.balanceOf(_self);
            }
        }
        return _tokenIds;
    }

    /***********************************|
    |            Public Burn            |
    |__________________________________*/

    /**
     * @notice Destroys a Stamp and releases the underlying DAI + Interest (Weight + Loot)
     * @param _tokenId      The ID of the token to burn
     */
    function burnStamp(uint256 _tokenId) public {
        // Burn Token
        _burn(msg.sender, _tokenId);

        // Payout Dai + Interest
        uint256 _tokenChai = chaiBalanceByTokenId[_tokenId];
        chaiBalanceByTokenId[_tokenId] = 0;
        _payoutFundedDai(msg.sender, _tokenChai);
    }

    /**
     * @notice Destroys multiple Stamps and releases the underlying DAI + Interest (Weight + Loot)
     * @param _tokenIds     The IDs of the tokens to burn
     */
    function burnStamps(uint256[] memory _tokenIds) public {
        uint256 _tokenId;
        uint256 _totalChai;
        for (uint256 i = 0; i < _tokenIds.length; ++i) {
            _tokenId = _tokenIds[i];

            // Burn Token
            _burn(msg.sender, _tokenId);

            // Payout Dai + Interest
            _totalChai = chaiBalanceByTokenId[_tokenId].add(_totalChai);
            chaiBalanceByTokenId[_tokenId] = 0;
        }
        _payoutFundedDai(msg.sender, _totalChai);
    }

    /***********************************|
    |            Only Owner             |
    |__________________________________*/

    /**
     * @dev Setup the DAI/CHAI contracts and configure the $TAMP contract
     */
    function setup(address _daiAddress, address _chaiAddress, uint256 _mintFee, uint256 _requiredFunding) public onlyOwner {
        // Set DAI as Funding Token
        dai = IERC20(_daiAddress);
        chai = IChai(_chaiAddress);

        // Setup Chai to Tokenize DAI Interest
        dai.approve(_chaiAddress, uint(-1));

        mintFee = _mintFee;
        requiredFunding = _requiredFunding;
    }

    /**
     * @dev Allows contract owner to withdraw any fees earned
     */
    function withdrawFees() public onlyOwner {
        uint256 _balance = address(this).balance;
        if (_balance > 0) {
            msg.sender.transfer(_balance);
        }
        if (collectedFees > 0) {
            _payoutFundedDai(msg.sender, collectedFees);
            collectedFees = 0;
        }
    }

    /**
     * @notice Transfers the ownership of the contract to new address
     * @param _newOwner Address of the new owner
     */
    function transferOwnership(address _newOwner) public onlyOwner {
        require(_newOwner != address(0), "E101");
        emit OwnershipTransferred(owner, _newOwner);
        owner = _newOwner;
    }

    /***********************************|
    |         Private Functions         |
    |__________________________________*/

    /**
     * @dev Collects the Required DAI from the users wallet during Minting
     * @param _from         The owner address to collect the DAI from
     * @param _requiredDai  The amount of DAI to collect from the user
     */
    function _collectRequiredDai(address _from, uint256 _requiredDai) internal {
        // Transfer DAI from User to Contract
        uint256 _userDaiBalance = dai.balanceOf(_from);
        require(_requiredDai <= _userDaiBalance, "E404");
        require(dai.transferFrom(_from, address(this), _requiredDai), "E405");
    }

    /**
     * @dev Pays out a specified amount of CHAI
     * @param _to           The owner address to pay out to
     * @param _totalChai    The total amount of CHAI to pay out
     */
    function _payoutFundedDai(address _to, uint256 _totalChai) internal {
        address _self = address(this);

        // Exit Chai and collect Dai + Interest
        chai.exit(_self, _totalChai);

        // Transfer Dai + Interest
        uint256 _receivedDai = dai.balanceOf(_self);
        require(dai.transferFrom(_self, _to, _receivedDai), "E405");
    }

    /**
     * @dev Pays out a specified amount of DAI
     * @param _to           The owner address to pay out to
     * @param _totalDai     The total amount of DAI to pay out
     */
    function _payoutStampedDai(address _to, uint256 _totalDai) internal returns (uint256) {
        address _self = address(this);

        // Collect Interest
        uint256 _chai = chai.draw(_self, _totalDai);

        // Transfer Interest
        uint256 _receivedDai = dai.balanceOf(_self);
        require(dai.transferFrom(_self, _to, _receivedDai), "E405");
        return _chai;
    }

    /**
     * @dev Calculates the amount of DAI held within a token during minting
     *      Note: Accounts for any contract fees
     * @param _tokenChai    The total amount of DAI used to fund the token
     * @return  The actual amount of DAI to fund the token - fees
     */
    function _totalChaiForToken(uint256 _tokenChai) internal returns (uint256) {
        if (mintFee == 0) { return _tokenChai; }
        uint256 _mintFee = _tokenChai.mul(mintFee).div(1e4);
        collectedFees = collectedFees.add(_mintFee);
        return _tokenChai.sub(_mintFee);
    }
}
/**
 * @notice Money Stamps Contract - Interest-Bearing NFTs with lexical stamps
 * <presented by OpenESQ || lexDAO>
 */
function mintStamps(address _to, uint256 _amount, string memory _stamp) public returns (uint256[] memory) {
    address _self = address(this);
    uint256 i;
    uint256 _tokenId;
    uint256 _totalDai;
    uint256[] memory _tokenIds = new uint256[](_amount);

    for (i = 0; i < _amount; ++i) {
        _totalDai = requiredFunding.add(_totalDai);

        _tokenId = (totalMintedTokens.add(i+1));
        _tokenIds[i] = _tokenId;
        _mint(_to, _tokenId);
	    _setTokenURI(_tokenId, _stamp);
    }
    totalMintedTokens = totalMintedTokens.add(_amount);

    if (_totalDai > 0) {
        // Transfer DAI from User to Contract
        _collectRequiredDai(msg.sender, _totalDai);

        uint256 _balance = chai.balanceOf(_self);
        for (i = 0; i < _amount; ++i) {
            _tokenId = _tokenIds[i];

            // Tokenize Interest
            chai.join(_self, requiredFunding);

            // Track Chai in each Token
             chaiBalanceByTokenId[_tokenId] = _totalChaiForToken(chai.balanceOf(_self).sub(_balance));
            _balance = chai.balanceOf(_self);
        }
    }
    return _tokenIds;
}

/**
 * @notice Mints multiple new Stamps
 * @param _to       The owner address to assign the new tokens to
 * @param _amount   The amount of tokens to mint
 * @param _stamp     Custom lexical stamp used to establish authenticity 
 * @return  The IDs of the newly minted tokens
 */