pragma solidity ^0.8.0; // SPDX-License-Identifier: MIT import "../core/DaoRegistry.sol"; import "../extensions/nft/NFT.sol"; import "../extensions/erc1155/ERC1155TokenExtension.sol"; import "../extensions/bank/Bank.sol"; import "../adapters/interfaces/IVoting.sol"; import "../guards/AdapterGuard.sol"; import "./modifiers/Reimbursable.sol"; import "../helpers/DaoHelper.sol"; import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/utils/Address.sol"; /** MIT License Copyright (c) 2020 Openlaw Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ contract TributeNFTContract is AdapterGuard, Reimbursable, IERC1155Receiver, IERC721Receiver { using Address for address payable; struct ProcessProposal { DaoRegistry dao; bytes32 proposalId; } struct ProposalDetails { // The proposal id. bytes32 id; // The applicant address (who will receive the DAO internal tokens and // become a member; this address may be different than the actual owner // of the ERC-721 token being provided as tribute). address applicant; // The address of the ERC-721 token that will be transferred to the DAO // in exchange for DAO internal tokens. address nftAddr; // The nft token identifier. uint256 nftTokenId; // The amount requested of DAO internal tokens (UNITS). uint256 requestAmount; } // Keeps track of all nft tribute proposals handled by each DAO. mapping(address => mapping(bytes32 => ProposalDetails)) public proposals; /** * @notice Configures the adapter for a particular DAO. * @notice Registers the DAO internal token UNITS with the DAO Bank. * @dev Only adapters registered to the DAO can execute the function call (or if the DAO is in creation mode). * @dev A DAO Bank extension must exist and be configured with proper access for this adapter. * @param dao The DAO address. */ function configureDao( DaoRegistry dao, address tokenAddrToMint ) external onlyAdapter(dao) { BankExtension(dao.getExtensionAddress(DaoHelper.BANK)) .registerPotentialNewInternalToken(dao, tokenAddrToMint); } /** * @notice Creates and sponsors a tribute proposal to start the voting process. * @dev Applicant address must not be reserved. * @dev Only members of the DAO can sponsor a tribute proposal. * @param dao The DAO address. * @param proposalId The proposal id (managed by the client). * @param applicant The applicant address (who will receive the DAO internal tokens and become a member). * @param nftAddr The address of the ERC-721 or ERC 1155 token that will be transferred to the DAO in exchange for DAO internal tokens. * @param nftTokenId The NFT token id. * @param requestAmount The amount requested of DAO internal tokens (UNITS). * @param data Additional information related to the tribute proposal. */ // slither-disable-next-line reentrancy-benign function submitProposal( DaoRegistry dao, bytes32 proposalId, address applicant, address nftAddr, uint256 nftTokenId, uint256 requestAmount, bytes memory data ) external reimbursable(dao) { require( DaoHelper.isNotReservedAddress(applicant), "applicant is reserved address" ); dao.submitProposal(proposalId); IVoting votingContract = IVoting( dao.getAdapterAddress(DaoHelper.VOTING) ); address sponsoredBy = votingContract.getSenderAddress( dao, address(this), data, msg.sender ); dao.sponsorProposal(proposalId, sponsoredBy, address(votingContract)); DaoHelper.potentialNewMember( applicant, dao, BankExtension(dao.getExtensionAddress(DaoHelper.BANK)) ); votingContract.startNewVotingForProposal(dao, proposalId, data); proposals[address(dao)][proposalId] = ProposalDetails( proposalId, applicant, nftAddr, nftTokenId, requestAmount ); } function _processProposal( DaoRegistry dao, bytes32 proposalId ) internal returns ( ProposalDetails storage proposal, IVoting.VotingState voteResult ) { proposal = proposals[address(dao)][proposalId]; require(proposal.id == proposalId, "proposal does not exist"); require( !dao.getProposalFlag( proposalId, DaoRegistry.ProposalFlag.PROCESSED ), "proposal already processed" ); IVoting votingContract = IVoting(dao.votingAdapter(proposalId)); require(address(votingContract) != address(0), "adapter not found"); voteResult = votingContract.voteResult(dao, proposalId); dao.processProposal(proposalId); //if proposal passes and its an erc721 token - use NFT Extension if (voteResult == IVoting.VotingState.PASS) { BankExtension bank = BankExtension( dao.getExtensionAddress(DaoHelper.BANK) ); require( bank.isInternalToken(DaoHelper.UNITS), "UNITS token is not an internal token" ); bank.addToBalance( dao, proposal.applicant, DaoHelper.UNITS, proposal.requestAmount ); return (proposal, voteResult); } else if ( voteResult == IVoting.VotingState.NOT_PASS || voteResult == IVoting.VotingState.TIE ) { return (proposal, voteResult); } else { revert("proposal has not been voted on yet"); } } /** * @notice required function from IERC1155 standard to be able to to receive tokens */ function onERC1155Received( address, address from, uint256 id, uint256 value, bytes calldata data ) external override returns (bytes4) { ProcessProposal memory ppS = abi.decode(data, (ProcessProposal)); ReimbursementData memory rData = ReimbursableLib.beforeExecution( ppS.dao ); ( ProposalDetails storage proposal, IVoting.VotingState voteResult ) = _processProposal(ppS.dao, ppS.proposalId); require(proposal.nftTokenId == id, "wrong NFT"); require(proposal.nftAddr == msg.sender, "wrong NFT addr"); if (voteResult == IVoting.VotingState.PASS) { address erc1155ExtAddr = ppS.dao.getExtensionAddress( DaoHelper.ERC1155_EXT ); IERC1155(msg.sender).safeTransferFrom( address(this), erc1155ExtAddr, id, value, "" ); } else { IERC1155(msg.sender).safeTransferFrom( address(this), from, id, value, "" ); } ReimbursableLib.afterExecution2(ppS.dao, rData, payable(from)); return this.onERC1155Received.selector; } /** * @notice required function from IERC1155 standard to be able to to batch receive tokens */ function onERC1155BatchReceived( address, address, uint256[] calldata, uint256[] calldata, bytes calldata ) external pure override returns (bytes4) { revert("not supported"); } /** * @notice Supports ERC-165 & ERC-1155 interfaces only. * @dev https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1155.md */ function supportsInterface( bytes4 interfaceID ) external pure override returns (bool) { return interfaceID == this.supportsInterface.selector || interfaceID == this.onERC1155Received.selector || interfaceID == this.onERC721Received.selector; } /** * @notice Required function from IERC721 standard to be able to receive assets to this contract address. * @notice This function is called from the NFT contract if the transfer was made using {safeTransferFrom} */ function onERC721Received( address, address from, uint256 tokenId, bytes calldata data ) external override returns (bytes4) { ProcessProposal memory ppS = abi.decode(data, (ProcessProposal)); ReimbursementData memory rData = ReimbursableLib.beforeExecution( ppS.dao ); ( ProposalDetails storage proposal, IVoting.VotingState voteResult ) = _processProposal(ppS.dao, ppS.proposalId); require(proposal.nftTokenId == tokenId, "wrong NFT"); require(proposal.nftAddr == msg.sender, "wrong NFT addr"); //if proposal passes and its an erc721 token - use NFT Extension if (voteResult == IVoting.VotingState.PASS) { NFTExtension nftExt = NFTExtension( ppS.dao.getExtensionAddress(DaoHelper.NFT) ); IERC721(msg.sender).approve(address(nftExt), proposal.nftTokenId); nftExt.collect(ppS.dao, proposal.nftAddr, proposal.nftTokenId); } else { IERC721(msg.sender).safeTransferFrom(address(this), from, tokenId); } ReimbursableLib.afterExecution2(ppS.dao, rData, payable(from)); return this.onERC721Received.selector; } /** * @notice Process an ERC721 onboarding proposal. * @notice This function must be called if NFT transfer was made using {transferFrom} which does not trigger the onERC721Received callback. * @notice If the proposal didn't not pass, the NFT is returned to applicant. * @param dao The DAO address. * @param proposalId The proposalId to be processed. */ function processERC721Proposal( DaoRegistry dao, bytes32 proposalId ) external reimbursable(dao) { ( ProposalDetails storage proposal, IVoting.VotingState voteResult ) = _processProposal(dao, proposalId); //if proposal passes and its an erc721 token - use NFT Extension if (voteResult == IVoting.VotingState.PASS) { NFTExtension nftExt = NFTExtension( dao.getExtensionAddress(DaoHelper.NFT) ); IERC721(proposal.nftAddr).approve( address(nftExt), proposal.nftTokenId ); nftExt.collect(dao, proposal.nftAddr, proposal.nftTokenId); } else { IERC721(proposal.nftAddr).safeTransferFrom( address(this), proposal.applicant, proposal.nftTokenId ); } } }