MintBountyFactory

Description:

Multi-signature wallet contract requiring multiple confirmations for transaction execution.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "src/MintBountyFactory.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/proxy/Clones.sol";
import "./MintBounty.sol";

/**
 * @title MintBountyFactory
 * @author ripe
 * @notice Factory contract for deploying MintBounty clones using EIP-1167
 * @dev Uses OpenZeppelin's Clones library for gas-efficient deployment
 */
contract MintBountyFactory {
    using Clones for address;

    /// @notice The implementation contract that all clones will delegate to
    address public immutable implementation;

    /// @notice Emitted when a new bounty contract is deployed
    event BountyContractDeployed(
        address indexed owner,
        address indexed bountyContract
    );

    /**
     * @notice Deploy the factory with its implementation contract
     * @dev The implementation is deployed once and reused by all clones
     */
    constructor() {
        implementation = address(new MintBounty());
    }

    /**
     * @notice Deploy a new MintBounty clone for the caller
     * @return bountyContract The address of the newly deployed clone
     */
    function deployBountyContract() external returns (address bountyContract) {
        // Deploy minimal proxy clone
        bountyContract = implementation.clone();

        // Initialize the clone with msg.sender as owner
        MintBounty(bountyContract).initialize(msg.sender);

        // Emit event for tracking
        emit BountyContractDeployed(msg.sender, bountyContract);
    }
}
"
    },
    "lib/openzeppelin-contracts/contracts/proxy/Clones.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (proxy/Clones.sol)

pragma solidity ^0.8.20;

import {Create2} from "../utils/Create2.sol";
import {Errors} from "../utils/Errors.sol";

/**
 * @dev https://eips.ethereum.org/EIPS/eip-1167[ERC-1167] is a standard for
 * deploying minimal proxy contracts, also known as "clones".
 *
 * > To simply and cheaply clone contract functionality in an immutable way, this standard specifies
 * > a minimal bytecode implementation that delegates all calls to a known, fixed address.
 *
 * The library includes functions to deploy a proxy using either `create` (traditional deployment) or `create2`
 * (salted deterministic deployment). It also includes functions to predict the addresses of clones deployed using the
 * deterministic method.
 */
library Clones {
    error CloneArgumentsTooLong();

    /**
     * @dev Deploys and returns the address of a clone that mimics the behavior of `implementation`.
     *
     * This function uses the create opcode, which should never revert.
     *
     * WARNING: This function does not check if `implementation` has code. A clone that points to an address
     * without code cannot be initialized. Initialization calls may appear to be successful when, in reality, they
     * have no effect and leave the clone uninitialized, allowing a third party to initialize it later.
     */
    function clone(address implementation) internal returns (address instance) {
        return clone(implementation, 0);
    }

    /**
     * @dev Same as {xref-Clones-clone-address-}[clone], but with a `value` parameter to send native currency
     * to the new contract.
     *
     * WARNING: This function does not check if `implementation` has code. A clone that points to an address
     * without code cannot be initialized. Initialization calls may appear to be successful when, in reality, they
     * have no effect and leave the clone uninitialized, allowing a third party to initialize it later.
     *
     * NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
     * to always have enough balance for new deployments. Consider exposing this function under a payable method.
     */
    function clone(address implementation, uint256 value) internal returns (address instance) {
        if (address(this).balance < value) {
            revert Errors.InsufficientBalance(address(this).balance, value);
        }
        assembly ("memory-safe") {
            // Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
            // of the `implementation` address with the bytecode before the address.
            mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
            // Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
            mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
            instance := create(value, 0x09, 0x37)
        }
        if (instance == address(0)) {
            revert Errors.FailedDeployment();
        }
    }

    /**
     * @dev Deploys and returns the address of a clone that mimics the behavior of `implementation`.
     *
     * This function uses the create2 opcode and a `salt` to deterministically deploy
     * the clone. Using the same `implementation` and `salt` multiple times will revert, since
     * the clones cannot be deployed twice at the same address.
     *
     * WARNING: This function does not check if `implementation` has code. A clone that points to an address
     * without code cannot be initialized. Initialization calls may appear to be successful when, in reality, they
     * have no effect and leave the clone uninitialized, allowing a third party to initialize it later.
     */
    function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) {
        return cloneDeterministic(implementation, salt, 0);
    }

    /**
     * @dev Same as {xref-Clones-cloneDeterministic-address-bytes32-}[cloneDeterministic], but with
     * a `value` parameter to send native currency to the new contract.
     *
     * WARNING: This function does not check if `implementation` has code. A clone that points to an address
     * without code cannot be initialized. Initialization calls may appear to be successful when, in reality, they
     * have no effect and leave the clone uninitialized, allowing a third party to initialize it later.
     *
     * NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
     * to always have enough balance for new deployments. Consider exposing this function under a payable method.
     */
    function cloneDeterministic(
        address implementation,
        bytes32 salt,
        uint256 value
    ) internal returns (address instance) {
        if (address(this).balance < value) {
            revert Errors.InsufficientBalance(address(this).balance, value);
        }
        assembly ("memory-safe") {
            // Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
            // of the `implementation` address with the bytecode before the address.
            mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
            // Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
            mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
            instance := create2(value, 0x09, 0x37, salt)
        }
        if (instance == address(0)) {
            revert Errors.FailedDeployment();
        }
    }

    /**
     * @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
     */
    function predictDeterministicAddress(
        address implementation,
        bytes32 salt,
        address deployer
    ) internal pure returns (address predicted) {
        assembly ("memory-safe") {
            let ptr := mload(0x40)
            mstore(add(ptr, 0x38), deployer)
            mstore(add(ptr, 0x24), 0x5af43d82803e903d91602b57fd5bf3ff)
            mstore(add(ptr, 0x14), implementation)
            mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73)
            mstore(add(ptr, 0x58), salt)
            mstore(add(ptr, 0x78), keccak256(add(ptr, 0x0c), 0x37))
            predicted := and(keccak256(add(ptr, 0x43), 0x55), 0xffffffffffffffffffffffffffffffffffffffff)
        }
    }

    /**
     * @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
     */
    function predictDeterministicAddress(
        address implementation,
        bytes32 salt
    ) internal view returns (address predicted) {
        return predictDeterministicAddress(implementation, salt, address(this));
    }

    /**
     * @dev Deploys and returns the address of a clone that mimics the behavior of `implementation` with custom
     * immutable arguments. These are provided through `args` and cannot be changed after deployment. To
     * access the arguments within the implementation, use {fetchCloneArgs}.
     *
     * This function uses the create opcode, which should never revert.
     *
     * WARNING: This function does not check if `implementation` has code. A clone that points to an address
     * without code cannot be initialized. Initialization calls may appear to be successful when, in reality, they
     * have no effect and leave the clone uninitialized, allowing a third party to initialize it later.
     */
    function cloneWithImmutableArgs(address implementation, bytes memory args) internal returns (address instance) {
        return cloneWithImmutableArgs(implementation, args, 0);
    }

    /**
     * @dev Same as {xref-Clones-cloneWithImmutableArgs-address-bytes-}[cloneWithImmutableArgs], but with a `value`
     * parameter to send native currency to the new contract.
     *
     * WARNING: This function does not check if `implementation` has code. A clone that points to an address
     * without code cannot be initialized. Initialization calls may appear to be successful when, in reality, they
     * have no effect and leave the clone uninitialized, allowing a third party to initialize it later.
     *
     * NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
     * to always have enough balance for new deployments. Consider exposing this function under a payable method.
     */
    function cloneWithImmutableArgs(
        address implementation,
        bytes memory args,
        uint256 value
    ) internal returns (address instance) {
        if (address(this).balance < value) {
            revert Errors.InsufficientBalance(address(this).balance, value);
        }
        bytes memory bytecode = _cloneCodeWithImmutableArgs(implementation, args);
        assembly ("memory-safe") {
            instance := create(value, add(bytecode, 0x20), mload(bytecode))
        }
        if (instance == address(0)) {
            revert Errors.FailedDeployment();
        }
    }

    /**
     * @dev Deploys and returns the address of a clone that mimics the behavior of `implementation` with custom
     * immutable arguments. These are provided through `args` and cannot be changed after deployment. To
     * access the arguments within the implementation, use {fetchCloneArgs}.
     *
     * This function uses the create2 opcode and a `salt` to deterministically deploy the clone. Using the same
     * `implementation`, `args` and `salt` multiple times will revert, since the clones cannot be deployed twice
     * at the same address.
     *
     * WARNING: This function does not check if `implementation` has code. A clone that points to an address
     * without code cannot be initialized. Initialization calls may appear to be successful when, in reality, they
     * have no effect and leave the clone uninitialized, allowing a third party to initialize it later.
     */
    function cloneDeterministicWithImmutableArgs(
        address implementation,
        bytes memory args,
        bytes32 salt
    ) internal returns (address instance) {
        return cloneDeterministicWithImmutableArgs(implementation, args, salt, 0);
    }

    /**
     * @dev Same as {xref-Clones-cloneDeterministicWithImmutableArgs-address-bytes-bytes32-}[cloneDeterministicWithImmutableArgs],
     * but with a `value` parameter to send native currency to the new contract.
     *
     * WARNING: This function does not check if `implementation` has code. A clone that points to an address
     * without code cannot be initialized. Initialization calls may appear to be successful when, in reality, they
     * have no effect and leave the clone uninitialized, allowing a third party to initialize it later.
     *
     * NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
     * to always have enough balance for new deployments. Consider exposing this function under a payable method.
     */
    function cloneDeterministicWithImmutableArgs(
        address implementation,
        bytes memory args,
        bytes32 salt,
        uint256 value
    ) internal returns (address instance) {
        bytes memory bytecode = _cloneCodeWithImmutableArgs(implementation, args);
        return Create2.deploy(value, salt, bytecode);
    }

    /**
     * @dev Computes the address of a clone deployed using {Clones-cloneDeterministicWithImmutableArgs}.
     */
    function predictDeterministicAddressWithImmutableArgs(
        address implementation,
        bytes memory args,
        bytes32 salt,
        address deployer
    ) internal pure returns (address predicted) {
        bytes memory bytecode = _cloneCodeWithImmutableArgs(implementation, args);
        return Create2.computeAddress(salt, keccak256(bytecode), deployer);
    }

    /**
     * @dev Computes the address of a clone deployed using {Clones-cloneDeterministicWithImmutableArgs}.
     */
    function predictDeterministicAddressWithImmutableArgs(
        address implementation,
        bytes memory args,
        bytes32 salt
    ) internal view returns (address predicted) {
        return predictDeterministicAddressWithImmutableArgs(implementation, args, salt, address(this));
    }

    /**
     * @dev Get the immutable args attached to a clone.
     *
     * - If `instance` is a clone that was deployed using `clone` or `cloneDeterministic`, this
     *   function will return an empty array.
     * - If `instance` is a clone that was deployed using `cloneWithImmutableArgs` or
     *   `cloneDeterministicWithImmutableArgs`, this function will return the args array used at
     *   creation.
     * - If `instance` is NOT a clone deployed using this library, the behavior is undefined. This
     *   function should only be used to check addresses that are known to be clones.
     */
    function fetchCloneArgs(address instance) internal view returns (bytes memory) {
        bytes memory result = new bytes(instance.code.length - 45); // revert if length is too short
        assembly ("memory-safe") {
            extcodecopy(instance, add(result, 32), 45, mload(result))
        }
        return result;
    }

    /**
     * @dev Helper that prepares the initcode of the proxy with immutable args.
     *
     * An assembly variant of this function requires copying the `args` array, which can be efficiently done using
     * `mcopy`. Unfortunately, that opcode is not available before cancun. A pure solidity implementation using
     * abi.encodePacked is more expensive but also more portable and easier to review.
     *
     * NOTE: https://eips.ethereum.org/EIPS/eip-170[EIP-170] limits the length of the contract code to 24576 bytes.
     * With the proxy code taking 45 bytes, that limits the length of the immutable args to 24531 bytes.
     */
    function _cloneCodeWithImmutableArgs(
        address implementation,
        bytes memory args
    ) private pure returns (bytes memory) {
        if (args.length > 24531) revert CloneArgumentsTooLong();
        return
            abi.encodePacked(
                hex"61",
                uint16(args.length + 45),
                hex"3d81600a3d39f3363d3d373d3d3d363d73",
                implementation,
                hex"5af43d82803e903d91602b57fd5bf3",
                args
            );
    }
}
"
    },
    "src/MintBounty.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "./interfaces/IMint.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/**
 * @title MintBounty
 * @author ripe
 * @notice Automated bounty system for minting digital artifacts from Mint protocol collections
 * @dev This contract incentivizes community members to execute mints on behalf of the owner
 * by offering artifact rewards and gas refunds. Designed specifically for daily artifact
 * drops on the Mint protocol (mint.vv.xyz) where timing is critical.
 *
 * The owner funds bounties with ETH and sets parameters like reward amounts and gas limits.
 * When new artifacts become available, any minter can execute the bounty to:
 * - Mint the total specified number of artifacts (artifactsToMint)
 * - Receive their reward artifacts immediately (minterReward amount)
 * - Get a full gas refund from the bounty balance (including base tx cost)
 * - Leave the owner's artifacts in the contract for later withdrawal (artifactsToMint - minterReward)
 *
 * This creates a win-win: owners never miss drops, minters earn artifacts for their service.
 */
contract MintBounty is ReentrancyGuard {
    // Constants for better readability (compile-time replaced, no gas cost)
    uint256 private constant BASE_TX_GAS = 21_000; // Ethereum base transaction cost
    uint256 private constant MINT_PRICE_MULTIPLIER = 60_000; // Artifact price = basefee * this
    uint256 private constant MIN_GAS_BUFFER = 200_000; // Minimum gas units to cover refund
    uint256 private constant ETH_TRANSFER_GAS = 9_000; // Approximate gas for ETH transfer

    struct Bounty {
        address recipient; // Where owner's artifacts go
        bool paused; // Is bounty paused (packed with address)
        uint96 lastMintedId; // Last successfully minted token ID (96 bits enough)
        uint256 artifactsToMint; // Total number of artifacts to mint (minter gets minterReward, owner gets the rest)
        uint256 minterReward; // Number of artifacts the minter keeps (must be less than artifactsToMint)
        uint256 maxArtifactPrice; // Max price per artifact in wei (e.g., 0.001 ETH = 1000000000000000 wei)
        uint256 balance; // ETH balance for mints + refunds
    }

    /// @notice The owner address who can manage all bounties
    address public owner;

    /// @notice Mapping from Mint collection address to its bounty configuration
    mapping(address => Bounty) public bounties;

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    modifier bountyExists(address tokenContract) {
        require(bounties[tokenContract].recipient != address(0), "No bounty");
        _;
    }

    error TransferFailed();

    // Internal helper to handle ETH transfers with error checking
    function _transferETH(address to, uint256 amount) internal {
        (bool success, ) = payable(to).call{value: amount}("");
        if (!success) revert TransferFailed();
    }

    /**
     * @notice Creates a new MintBounty contract
     * @dev For direct deployment, call initialize(msg.sender) after deployment.
     *      For factory deployment, clones will call initialize with the user's address.
     */
    constructor() {
        // Constructor left empty to support both direct and clone deployment patterns
        // Owner must be set via initialize() function
    }

    /**
     * @notice Initialize the contract with an owner address
     * @dev Used by factory for clone deployment. Can only be called once.
     * @param _owner The address that will own this bounty contract
     */
    function initialize(address _owner) external {
        require(owner == address(0), "Already initialized");
        require(_owner != address(0), "Invalid owner");
        owner = _owner;
    }

    /**
     * @notice Creates or updates a bounty for a specific Mint collection
     * @dev Overwrites any existing bounty for the same tokenContract. Starts with lastMintedId=0
     *      to allow minting the currently available token immediately.
     * @param tokenContract The Mint collection contract address
     * @param recipient Address to receive the owner's artifacts when withdrawn
     * @param artifactsToMint Total number of artifacts to mint (e.g., 3 total: 1 for minter, 2 for owner)
     * @param minterReward Number of artifacts the minter keeps as payment (e.g., 1 out of 3 total)
     * @param maxArtifactPrice Maximum price per artifact in wei (e.g., 0.001 ETH = 1000000000000000 wei)
     *                         Since artifact price = block.basefee * 60000, this also limits gas price.
     *                         Example: At 10 gwei basefee, artifact costs 0.0006 ETH
     */
    function createBounty(
        address tokenContract,
        address recipient,
        uint256 artifactsToMint,
        uint256 minterReward,
        uint256 maxArtifactPrice
    ) external payable onlyOwner {
        require(minterReward < artifactsToMint, "Invalid reward");
        require(recipient != address(0), "Invalid recipient");

        bounties[tokenContract] = Bounty({
            recipient: recipient,
            paused: false,
            lastMintedId: 0, // Start at 0 to allow minting current token
            artifactsToMint: artifactsToMint,
            minterReward: minterReward,
            maxArtifactPrice: maxArtifactPrice,
            balance: msg.value
        });
    }

    /**
     * @notice Claims a bounty by minting artifacts and distributing rewards
     * @dev Mints artifacts, sends minter their reward, refunds gas (including base tx cost), keeps owner's portion
     * @param tokenContract The Mint collection to claim the bounty for
     *
     * Gas Refund Details:
     * - Refunds claiming gas + 21,000 base transaction cost + ETH transfer gas
     * - Minter's net cost is effectively zero (maybe a few hundred gas units)
     *
     * Requirements:
     * - Bounty must exist and not be paused
     * - New token ID must be available (higher than last minted)
     * - Current gas price must be <= maxGasPrice
     * - Bounty must have sufficient balance for mint cost + gas refund
     * - Mint window must still be open
     */
    function claimBounty(address tokenContract) external nonReentrant {
        // Reserve gas for the final ETH transfer upfront
        uint256 gasStart = gasleft() - ETH_TRANSFER_GAS;

        // Check claimability and get required values
        (uint256 latestId, uint256 totalCost) = _requireClaimable(
            tokenContract
        );

        Bounty storage bounty = bounties[tokenContract];

        // Mint artifacts to this contract
        IMint(tokenContract).mint{value: totalCost}(
            latestId,
            bounty.artifactsToMint
        );

        // Transfer only minter's reward (owner withdraws their portion later)
        IERC1155(tokenContract).safeTransferFrom(
            address(this),
            msg.sender,
            latestId,
            bounty.minterReward,
            ""
        );

        // Update state
        bounty.lastMintedId = uint96(latestId);

        // Calculate gas refund (capped at remaining balance after mint)
        // Don't include ETH_TRANSFER_GAS since we reserved it upfront
        uint256 gasUsed = (gasStart - gasleft() + BASE_TX_GAS) * tx.gasprice;
        uint256 remainingBalance = bounty.balance - totalCost;
        if (gasUsed > remainingBalance) {
            gasUsed = remainingBalance;
        }

        // Single balance update for both mint cost and gas refund
        bounty.balance = remainingBalance - gasUsed;

        // Transfer gas refund (we reserved gas for this)
        _transferETH(msg.sender, gasUsed);
    }

    function _requireClaimable(
        address tokenContract
    ) internal view returns (uint256 latestId, uint256 totalCost) {
        bool claimable;
        string memory reason;
        (claimable, latestId, totalCost, reason) = _checkClaimable(
            tokenContract
        );
        require(claimable, reason);
    }

    function _checkClaimable(
        address tokenContract
    )
        internal
        view
        returns (
            bool claimable,
            uint256 latestId,
            uint256 totalCost,
            string memory reason
        )
    {
        Bounty memory bounty = bounties[tokenContract];

        if (bounty.recipient == address(0)) {
            return (false, 0, 0, "No bounty");
        }

        if (bounty.paused) {
            return (false, 0, 0, "Bounty paused");
        }

        // Check if current artifact price exceeds maximum
        uint256 currentBaseFee = block.basefee;
        uint256 artifactPrice = currentBaseFee * MINT_PRICE_MULTIPLIER;
        if (artifactPrice > bounty.maxArtifactPrice) {
            return (false, 0, 0, "Artifact price too high");
        }

        latestId = IMint(tokenContract).latestTokenId();
        if (latestId <= bounty.lastMintedId) {
            return (false, 0, 0, "No new token");
        }

        totalCost = artifactPrice * bounty.artifactsToMint;

        // Ensure enough balance for mint + reasonable gas refund
        uint256 minRequired = totalCost + (MIN_GAS_BUFFER * tx.gasprice);
        if (bounty.balance < minRequired) {
            return (
                false,
                latestId,
                totalCost,
                "Insufficient funds for mint and refund"
            );
        }

        uint256 mintCloseTime = IMint(tokenContract).mintOpenUntil(latestId);
        if (block.timestamp >= mintCloseTime) {
            return (false, latestId, totalCost, "Mint closed");
        }

        return (true, latestId, totalCost, "");
    }

    /**
     * @notice Updates all bounty parameters in a single transaction
     * @dev More gas efficient than calling multiple update functions
     * @param tokenContract The Mint collection contract
     * @param recipient New address to receive owner's artifacts
     * @param paused Whether the bounty should be paused
     * @param artifactsToMint New total number of artifacts to mint (minter + owner combined)
     * @param minterReward New number of artifacts for minter (must be less than artifactsToMint)
     * @param maxArtifactPrice New maximum price per artifact in wei
     */
    function updateBounty(
        address tokenContract,
        address recipient,
        bool paused,
        uint256 artifactsToMint,
        uint256 minterReward,
        uint256 maxArtifactPrice
    ) external onlyOwner bountyExists(tokenContract) {
        require(recipient != address(0), "Invalid recipient");
        require(minterReward < artifactsToMint, "Invalid reward");

        Bounty storage bounty = bounties[tokenContract];
        bounty.recipient = recipient;
        bounty.paused = paused;
        bounty.artifactsToMint = artifactsToMint;
        bounty.minterReward = minterReward;
        bounty.maxArtifactPrice = maxArtifactPrice;
    }

    /**
     * @notice Adds ETH to an existing bounty's balance
     * @param tokenContract The Mint collection contract to fund
     */
    function fundBounty(address tokenContract) external payable onlyOwner {
        require(bounties[tokenContract].recipient != address(0), "No bounty");
        bounties[tokenContract].balance += msg.value;
    }

    /**
     * @notice Withdraws ETH from a bounty's balance back to owner
     * @param tokenContract The Mint collection contract
     * @param amount Amount of ETH to withdraw in wei
     */
    function withdrawBalance(
        address tokenContract,
        uint256 amount
    ) external onlyOwner bountyExists(tokenContract) {
        Bounty storage bounty = bounties[tokenContract];
        require(bounty.balance >= amount, "Insufficient balance");

        bounty.balance -= amount;
        _transferETH(owner, amount);
    }

    /**
     * @notice Withdraws accumulated artifacts from the contract to a recipient
     * @dev Transfers all available balance for each specified token ID
     * @param tokenContract The Mint collection contract holding the artifacts
     * @param tokenIds Array of token IDs to withdraw
     * @param recipient Address to receive the artifacts (can differ from bounty recipient)
     */
    function withdrawArtifacts(
        address tokenContract,
        uint256[] calldata tokenIds,
        address recipient
    ) external onlyOwner {
        for (uint256 i = 0; i < tokenIds.length; i++) {
            uint256 balance = IERC1155(tokenContract).balanceOf(
                address(this),
                tokenIds[i]
            );
            if (balance > 0) {
                IERC1155(tokenContract).safeTransferFrom(
                    address(this),
                    recipient,
                    tokenIds[i],
                    balance,
                    ""
                );
            }
        }
    }

    /**
     * @notice Checks if a bounty can currently be claimed
     * @param tokenContract The Mint collection to check
     * @return bool True if all conditions are met for claiming
     */
    function isBountyClaimable(
        address tokenContract
    ) external view returns (bool) {
        Bounty memory bounty = bounties[tokenContract];

        // Quick checks without calculating costs
        if (bounty.recipient == address(0) || bounty.paused) return false;

        // Check artifact price
        uint256 artifactPrice = block.basefee * MINT_PRICE_MULTIPLIER;
        if (artifactPrice > bounty.maxArtifactPrice) return false;

        uint256 latestId = IMint(tokenContract).latestTokenId();
        if (latestId <= bounty.lastMintedId) return false;

        // Check mint window
        uint256 mintCloseTime = IMint(tokenContract).mintOpenUntil(latestId);
        if (block.timestamp >= mintCloseTime) return false;

        // Check sufficient funds for mint + gas refund
        uint256 totalCost = artifactPrice * bounty.artifactsToMint;
        uint256 minRequired = totalCost + (MIN_GAS_BUFFER * tx.gasprice);
        return bounty.balance >= minRequired;
    }

    /**
     * @notice Handles receipt of a single ERC1155 token type
     * @dev Required for the contract to receive ERC1155 tokens
     * @return bytes4 The selector to confirm token transfer acceptance
     */
    function onERC1155Received(
        address,
        address,
        uint256,
        uint256,
        bytes calldata
    ) external pure returns (bytes4) {
        return this.onERC1155Received.selector;
    }

    /**
     * @notice Handles receipt of multiple ERC1155 token types
     * @dev Required for the contract to receive batch ERC1155 transfers
     * @return bytes4 The selector to confirm token transfer acceptance
     */
    function onERC1155BatchReceived(
        address,
        address,
        uint256[] calldata,
        uint256[] calldata,
        bytes calldata
    ) external pure returns (bytes4) {
        return this.onERC1155BatchReceived.selector;
    }
}
"
    },
    "lib/openzeppelin-contracts/contracts/utils/Create2.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/Create2.sol)

pragma solidity ^0.8.20;

import {Errors} from "./Errors.sol";

/**
 * @dev Helper to make usage of the `CREATE2` EVM opcode easier and safer.
 * `CREATE2` can be used to compute in advance the address where a smart
 * contract will be deployed, which allows for interesting new mechanisms known
 * as 'counterfactual interactions'.
 *
 * See the https://eips.ethereum.org/EIPS/eip-1014#motivation[EIP] for more
 * information.
 */
library Create2 {
    /**
     * @dev There's no code to deploy.
     */
    error Create2EmptyBytecode();

    /**
     * @dev Deploys a contract using `CREATE2`. The address where the contract
     * will be deployed can be known in advance via {computeAddress}.
     *
     * The bytecode for a contract can be obtained from Solidity with
     * `type(contractName).creationCode`.
     *
     * Requirements:
     *
     * - `bytecode` must not be empty.
     * - `salt` must have not been used for `bytecode` already.
     * - the factory must have a balance of at least `amount`.
     * - if `amount` is non-zero, `bytecode` must have a `payable` constructor.
     */
    function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) internal returns (address addr) {
        if (address(this).balance < amount) {
            revert Errors.InsufficientBalance(address(this).balance, amount);
        }
        if (bytecode.length == 0) {
            revert Create2EmptyBytecode();
        }
        assembly ("memory-safe") {
            addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt)
            // if no address was created, and returndata is not empty, bubble revert
            if and(iszero(addr), not(iszero(returndatasize()))) {
                let p := mload(0x40)
                returndatacopy(p, 0, returndatasize())
                revert(p, returndatasize())
            }
        }
        if (addr == address(0)) {
            revert Errors.FailedDeployment();
        }
    }

    /**
     * @dev Returns the address where a contract will be stored if deployed via {deploy}. Any change in the
     * `bytecodeHash` or `salt` will result in a new destination address.
     */
    function computeAddress(bytes32 salt, bytes32 bytecodeHash) internal view returns (address) {
        return computeAddress(salt, bytecodeHash, address(this));
    }

    /**
     * @dev Returns the address where a contract will be stored if deployed via {deploy} from a contract located at
     * `deployer`. If `deployer` is this contract's address, returns the same value as {computeAddress}.
     */
    function computeAddress(bytes32 salt, bytes32 bytecodeHash, address deployer) internal pure returns (address addr) {
        assembly ("memory-safe") {
            let ptr := mload(0x40) // Get free memory pointer

            // |                   | ↓ ptr ...  ↓ ptr + 0x0B (start) ...  ↓ ptr + 0x20 ...  ↓ ptr + 0x40 ...   |
            // |-------------------|---------------------------------------------------------------------------|
            // | bytecodeHash      |                                                        CCCCCCCCCCCCC...CC |
            // | salt              |                                      BBBBBBBBBBBBB...BB                   |
            // | deployer          | 000000...0000AAAAAAAAAAAAAAAAAAA...AA                                     |
            // | 0xFF              |            FF                                                             |
            // |-------------------|---------------------------------------------------------------------------|
            // | memory            | 000000...00FFAAAAAAAAAAAAAAAAAAA...AABBBBBBBBBBBBB...BBCCCCCCCCCCCCC...CC |
            // | keccak(start, 85) |            ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ |

            mstore(add(ptr, 0x40), bytecodeHash)
            mstore(add(ptr, 0x20), salt)
            mstore(ptr, deployer) // Right-aligned with 12 preceding garbage bytes
            let start := add(ptr, 0x0b) // The hashed data starts at the final garbage byte which we will set to 0xff
            mstore8(start, 0xff)
            addr := and(keccak256(start, 85), 0xffffffffffffffffffffffffffffffffffffffff)
        }
    }
}
"
    },
    "lib/openzeppelin-contracts/contracts/utils/Errors.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/Errors.sol)

pragma solidity ^0.8.20;

/**
 * @dev Collection of common custom errors used in multiple contracts
 *
 * IMPORTANT: Backwards compatibility is not guaranteed in future versions of the library.
 * It is recommended to avoid relying on the error API for critical functionality.
 *
 * _Available since v5.1._
 */
library Errors {
    /**
     * @dev The ETH balance of the account is not enough to perform the operation.
     */
    error InsufficientBalance(uint256 balance, uint256 needed);

    /**
     * @dev A call to an address target failed. The target may have reverted.
     */
    error FailedCall();

    /**
     * @dev The deployment failed.
     */
    error FailedDeployment();

    /**
     * @dev A necessary precompile is missing.
     */
    error MissingPrecompile(address);
}
"
    },
    "src/interfaces/IMint.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IMint {
    function latestTokenId() external view returns (uint256);
    function mint(uint256 tokenId, uint256 amount) external payable;
    function mintOpenUntil(uint256 tokenId) external view returns (uint256);
}

interface IERC1155 {
    function safeTransferFrom(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes calldata data
    ) external;
    
    function balanceOf(address account, uint256 id) external view returns (uint256);
}"
    },
    "lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/ReentrancyGuard.sol)

pragma solidity ^0.8.20;

/**
 * @dev Contract module that helps prevent reentrant calls to a function.
 *
 * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
 * available, which can be applied to functions to make sure there are no nested
 * (reentrant) calls to them.
 *
 * Note that because there is a single `nonReentrant` guard, functions marked as
 * `nonReentrant` may not call one another. This can be worked around by making
 * those functions `private`, and then adding `external` `nonReentrant` entry
 * points to them.
 *
 * TIP: If EIP-1153 (transient storage) is available on the chain you're deploying at,
 * consider using {ReentrancyGuardTransient} instead.
 *
 * TIP: If you would like to learn more about reentrancy and alternative ways
 * to protect against it, check out our blog post
 * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
 */
abstract contract ReentrancyGuard {
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant NOT_ENTERED = 1;
    uint256 private constant ENTERED = 2;

    uint256 private _status;

    /**
     * @dev Unauthorized reentrant call.
     */
    error ReentrancyGuardReentrantCall();

    constructor() {
        _status = NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and making it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    function _nonReentrantBefore() private {
        // On the first call to nonReentrant, _status will be NOT_ENTERED
        if (_status == ENTERED) {
            revert ReentrancyGuardReentrantCall();
        }

        // Any calls to nonReentrant after this point will fail
        _status = ENTERED;
    }

    function _nonReentrantAfter() private {
        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _status = NOT_ENTERED;
    }

    /**
     * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
     * `nonReentrant` function in the call stack.
     */
    function _reentrancyGuardEntered() internal view returns (bool) {
        return _status == ENTERED;
    }
}
"
    }
  },
  "settings": {
    "remappings": [
      "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/",
      "forge-std/=lib/forge-std/src/",
      "erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/",
      "halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/",
      "openzeppelin-contracts/=lib/openzeppelin-contracts/"
    ],
    "optimizer": {
      "enabled": true,
      "runs": 200
    },
    "metadata": {
      "useLiteralContent": false,
      "bytecodeHash": "ipfs",
      "appendCBOR": true
    },
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "devdoc",
          "userdoc",
          "metadata",
          "abi"
        ]
      }
    },
    "evmVersion": "cancun",
    "viaIR": false
  }
}}

Tags:
Multisig, Upgradeable, Multi-Signature, Factory|addr:0x1bf79888027b7eee2e5b30890dbfd9157eb4c06a|verified:true|block:23385383|tx:0x2fe27cf17d9873b0733924eae46db281aec11879e712fddb122d895e7a68c956|first_check:1758186519

Submitted on: 2025-09-18 11:08:41

Comments

Log in to comment.

No comments yet.