StandardTokenLaunchRequirements

0xb66009788ea741b366ba852762a2821b1f3f7418

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
6,240 bytes
ABI entries
45
10 read · 9 write
License
none

Contract information

Address
0xb66009788ea741b366ba852762a2821b1f3f7418
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0x9b0c6422f8…5fdb288e31

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (10)

bondLockDuration()uint64
creatorBondAmount()uint256
factory()address
getReservation(uint256)tuple
lifecycleManager()address
owner()address
pendingOwner()address
reservationKey(uint256)bytes32
staking()address
templateRegistry()address

Events (9)

CreatorBondConfiguredCreatorBondLockedCreatorBondReturnedEquiStakeReleasedEquiStakeReservedFactoryConfiguredLifecycleManagerConfiguredOwnershipTransferStartedOwnershipTransferred

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "initialOwner",
        "type": "address"
      },
      {
        "internalType": "contract EquifoldStaking",
        "name": "staking_",
        "type": "address"
      },
      {
        "internalType": "contract LaunchTemplateRegistry",
        "name": "templateRegistry_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AlreadyConfigured",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "AlreadyReserved",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "BondAlreadyReturned",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "BondStillLocked",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "BondTransferMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidBondConfig",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidProject",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoBondEscrowed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotCreator",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotFactory",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotLifecycleManager",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotReserved",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint64",
        "name": "lockDuration",
        "type": "uint64"
      }
    ],
    "name": "CreatorBondConfigured",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "projectNumber",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint64",
        "name": "unlockAt",
        "type": "uint64"
      }
    ],
    "name": "CreatorBondLocked",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "projectNumber",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "CreatorBondReturned",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "projectNumber",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "reservationKey",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "EquiStakeReleased",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "projectNumber",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "reservationKey",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "EquiStakeReserved",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "factory",
        "type": "address"
      }
    ],
    "name": "FactoryConfigured",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "lifecycleManager",
        "type": "address"
      }
    ],
    "name": "LifecycleManagerConfigured",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferStarted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "bondLockDuration",
    "outputs": [
      {
        "internalType": "uint64",
        "name": "",
        "type": "uint64"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "projectNumber",
        "type": "uint256"
      }
    ],
    "name": "claimExpiredBond",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "creatorBondAmount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "factory",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "projectNumber",
        "type": "uint256"
      }
    ],
    "name": "getReservation",
    "outputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "creator",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "amount",
            "type": "uint256"
          },
          {
            "internalType": "bytes32",
            "name": "reservationKey",
            "type": "bytes32"
          },
          {
            "internalType": "bool",
            "name": "released",
            "type": "bool"
          },
          {
            "internalType": "uint256",
            "name": "bondAmount",
            "type": "uint256"
          },
          {
            "internalType": "uint64",
            "name": "bondUnlockAt",
            "type": "uint64"
          },
          {
            "internalType": "bool",
            "name": "bondReturned",
            "type": "bool"
          }
        ],
        "internalType": "struct StandardTokenLaunchRequirements.LaunchReservation",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "lifecycleManager",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "projectNumber",
        "type": "uint256"
      }
    ],
    "name": "releaseLaunch",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "projectNumber",
        "type": "uint256"
      }
    ],
    "name": "reservationKey",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "projectNumber",
        "type": "uint256"
      },
      {
        "internalType": "bytes32",
        "name": "templateKey",
        "type": "bytes32"
      },
      {
        "internalType": "uint32",
        "name": "templateVersion",
        "type": "uint32"
      }
    ],
    "name": "reserveLaunch",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "uint64",
        "name": "lockDuration",
        "type": "uint64"
      }
    ],
    "name": "setCreatorBond",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "factory_",
        "type": "address"
      }
    ],
    "name": "setFactory",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "lifecycleManager_",
        "type": "address"
      }
    ],
    "name": "setLifecycleManager",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "staking",
    "outputs": [
      {
        "internalType": "contract EquifoldStaking",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "templateRegistry",
    "outputs": [
      {
        "internalType": "contract LaunchTemplateRegistry",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { Ownable2Step, Ownable } from "@openzeppelin/contracts/access/Ownable2Step.sol";
import { ILaunchRequirements } from "../interfaces/ILaunchRequirements.sol";
import { EquifoldStaking } from "./EquifoldStaking.sol";
import { LaunchTemplateRegistry } from "./LaunchTemplateRegistry.sol";

/// @title Standard-token EQUI launch requirements
/// @notice Reserves the template-defined external EQUI stake through a product-neutral key, and
///         escrows a refundable EQUI creator bond (skin-in-the-game) that is returned at
///         graduation or reclaimable by the creator once the lock expires.
contract StandardTokenLaunchRequirements is ILaunchRequirements, Ownable2Step {
    using SafeERC20 for IERC20;

    error AlreadyConfigured();
    error AlreadyReserved();
    error BondAlreadyReturned();
    error BondStillLocked();
    error BondTransferMismatch();
    error InvalidAddress();
    error InvalidBondConfig();
    error InvalidProject();
    error NoBondEscrowed();
    error NotCreator();
    error NotFactory();
    error NotLifecycleManager();
    error NotReserved();

    uint64 internal constant MAX_BOND_LOCK_DURATION = 365 days;

    struct LaunchReservation {
        address creator;
        uint256 amount;
        bytes32 reservationKey;
        bool released;
        uint256 bondAmount;
        uint64 bondUnlockAt;
        bool bondReturned;
    }

    EquifoldStaking public immutable staking;
    LaunchTemplateRegistry public immutable templateRegistry;
    address public factory;
    address public lifecycleManager;
    /// @notice EQUI bond pulled from the creator at launch. Zero disables the bond entirely.
    uint256 public creatorBondAmount;
    /// @notice How long the bond stays locked before the creator can self-reclaim a never-graduated launch.
    uint64 public bondLockDuration;
    mapping(uint256 projectNumber => LaunchReservation reservation) private _reservations;

    event FactoryConfigured(address indexed factory);
    event LifecycleManagerConfigured(address indexed lifecycleManager);
    event CreatorBondConfigured(uint256 amount, uint64 lockDuration);
    event CreatorBondLocked(
        uint256 indexed projectNumber, address indexed creator, uint256 amount, uint64 unlockAt
    );
    event CreatorBondReturned(
        uint256 indexed projectNumber, address indexed creator, uint256 amount
    );
    event EquiStakeReserved(
        uint256 indexed projectNumber,
        address indexed creator,
        bytes32 indexed reservationKey,
        uint256 amount
    );
    event EquiStakeReleased(
        uint256 indexed projectNumber,
        address indexed creator,
        bytes32 indexed reservationKey,
        uint256 amount
    );

    constructor(
        address initialOwner,
        EquifoldStaking staking_,
        LaunchTemplateRegistry templateRegistry_
    ) Ownable(initialOwner) {
        if (
            initialOwner == address(0) || address(staking_) == address(0)
                || address(templateRegistry_) == address(0) || address(staking_).code.length == 0
                || address(templateRegistry_).code.length == 0
        ) revert InvalidAddress();
        staking = staking_;
        templateRegistry = templateRegistry_;
    }

    function setFactory(
        address factory_
    ) external onlyOwner {
        if (factory != address(0)) revert AlreadyConfigured();
        _validateContract(factory_);
        factory = factory_;
        emit FactoryConfigured(factory_);
    }

    function setLifecycleManager(
        address lifecycleManager_
    ) external onlyOwner {
        if (lifecycleManager != address(0)) revert AlreadyConfigured();
        _validateContract(lifecycleManager_);
        lifecycleManager = lifecycleManager_;
        emit LifecycleManagerConfigured(lifecycleManager_);
    }

    /// @notice Sets the refundable EQUI creator bond. `amount` of 0 disables the bond. When enabled,
    ///         `lockDuration` must be in (0, 365 days]; it bounds how long a never-graduated launch's
    ///         bond stays locked before the creator can self-reclaim it.
    function setCreatorBond(
        uint256 amount,
        uint64 lockDuration
    ) external onlyOwner {
        if (amount != 0 && (lockDuration == 0 || lockDuration > MAX_BOND_LOCK_DURATION)) {
            revert InvalidBondConfig();
        }
        creatorBondAmount = amount;
        bondLockDuration = amount == 0 ? 0 : lockDuration;
        emit CreatorBondConfigured(amount, bondLockDuration);
    }

    function reserveLaunch(
        address creator,
        uint256 projectNumber,
        bytes32 templateKey,
        uint32 templateVersion
    ) external {
        if (msg.sender != factory) revert NotFactory();
        if (creator == address(0) || projectNumber == 0) revert InvalidProject();
        if (_reservations[projectNumber].creator != address(0)) revert AlreadyReserved();

        uint256 amount = templateRegistry.getTemplate(templateKey, templateVersion).stakeRequirement;
        bytes32 key = reservationKey(projectNumber);
        if (amount != 0) staking.reserveStake(creator, key, amount);

        uint256 bond = creatorBondAmount;
        uint64 unlockAt;
        if (bond != 0) {
            unlockAt = uint64(block.timestamp) + bondLockDuration;
            IERC20 equi = staking.stakeToken();
            uint256 balanceBefore = equi.balanceOf(address(this));
            equi.safeTransferFrom(creator, address(this), bond);
            if (equi.balanceOf(address(this)) - balanceBefore != bond) {
                revert BondTransferMismatch();
            }
            emit CreatorBondLocked(projectNumber, creator, bond, unlockAt);
        }

        _reservations[projectNumber] = LaunchReservation({
            creator: creator,
            amount: amount,
            reservationKey: key,
            released: false,
            bondAmount: bond,
            bondUnlockAt: unlockAt,
            bondReturned: false
        });
        emit EquiStakeReserved(projectNumber, creator, key, amount);
    }

    function releaseLaunch(
        uint256 projectNumber
    ) external {
        if (msg.sender != lifecycleManager) revert NotLifecycleManager();
        LaunchReservation storage reservation = _reservations[projectNumber];
        if (reservation.creator == address(0) || reservation.released) revert NotReserved();
        reservation.released = true;
        if (reservation.amount != 0) staking.releaseStake(reservation.reservationKey);
        emit EquiStakeReleased(
            projectNumber, reservation.creator, reservation.reservationKey, reservation.amount
        );
        if (reservation.bondAmount != 0 && !reservation.bondReturned) {
            _returnBond(projectNumber, reservation);
        }
    }

    /// @notice Lets the creator reclaim their bond once the lock expires if the launch never graduated.
    function claimExpiredBond(
        uint256 projectNumber
    ) external {
        LaunchReservation storage reservation = _reservations[projectNumber];
        if (reservation.creator != msg.sender) revert NotCreator();
        if (reservation.bondAmount == 0) revert NoBondEscrowed();
        if (reservation.bondReturned) revert BondAlreadyReturned();
        if (block.timestamp < reservation.bondUnlockAt) revert BondStillLocked();
        _returnBond(projectNumber, reservation);
    }

    /// @dev Returns the escrowed bond to its creator. Effects precede the transfer (CEI); the
    ///      `bondReturned` flag makes double-return impossible from either release path.
    function _returnBond(
        uint256 projectNumber,
        LaunchReservation storage reservation
    ) private {
        reservation.bondReturned = true;
        uint256 bond = reservation.bondAmount;
        staking.stakeToken().safeTransfer(reservation.creator, bond);
        emit CreatorBondReturned(projectNumber, reservation.creator, bond);
    }

    function reservationKey(
        uint256 projectNumber
    ) public view returns (bytes32) {
        return keccak256(
            abi.encode("EQUIFOLD_STANDARD_TOKEN", block.chainid, address(this), projectNumber)
        );
    }

    function getReservation(
        uint256 projectNumber
    ) external view returns (LaunchReservation memory) {
        return _reservations[projectNumber];
    }

    function _validateContract(
        address target
    ) private view {
        if (target == address(0) || target.code.length == 0) revert InvalidAddress();
    }
}
Chain explorer5586msChain node84ms