Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions contracts/script/L2Genesis.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ contract GenesisEthscriptions is Ethscriptions {
require(ethscriptions[params.transactionHash].creator == address(0), "Ethscription already exists");

// Check protocol uniqueness using content URI hash
if (contentUriExists[params.contentUriHash]) {
if (firstEthscriptionByContentUri[params.contentUriHash] != bytes32(0)) {
if (!params.esip6) revert DuplicateContentUri();
}

// Store content and get content SHA (reusing parent's helper)
bytes32 contentSha = _storeContent(params.content);

// Mark content URI as used
contentUriExists[params.contentUriHash] = true;
// Mark content URI as used by storing this ethscription's tx hash
firstEthscriptionByContentUri[params.contentUriHash] = params.transactionHash;

// Set all values including genesis-specific ones
ethscriptions[params.transactionHash] = Ethscription({
Expand Down Expand Up @@ -341,7 +341,7 @@ contract L2Genesis is Script {
params.mimeSubtype = vm.parseJsonString(json, string.concat(basePath, ".mime_subtype"));
params.esip6 = vm.parseJsonBool(json, string.concat(basePath, ".esip6"));
params.protocolParams = Ethscriptions.ProtocolParams({
protocol: "",
protocolName: "",
operation: "",
data: ""
});
Expand Down
793 changes: 307 additions & 486 deletions contracts/src/Ethscriptions.sol

Large diffs are not rendered by default.

80 changes: 65 additions & 15 deletions contracts/src/EthscriptionsERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,97 @@ import "./ERC20NullOwnerCappedUpgradeable.sol";
import "./libraries/Predeploys.sol";

/// @title EthscriptionsERC20
/// @notice ERC20 with cap that supports null address ownership; only TokenManager can mint/transfer
/// @notice ERC20 with cap that supports null address ownership
/// @dev Only TokenManager can mint/transfer. User-initiated transfers are disabled.
contract EthscriptionsERC20 is ERC20NullOwnerCappedUpgradeable {

// =============================================================
// CONSTANTS
// =============================================================

/// @notice The TokenManager contract that controls this token
address public constant tokenManager = Predeploys.TOKEN_MANAGER;
bytes32 public deployTxHash; // The ethscription hash that deployed this token

// =============================================================
// STATE VARIABLES
// =============================================================

/// @notice The ethscription hash that deployed this token
bytes32 public deployTxHash;

// =============================================================
// CUSTOM ERRORS
// =============================================================

error OnlyTokenManager();
error TransfersOnlyViaEthscriptions();
error ApprovalsNotAllowed();

// =============================================================
// MODIFIERS
// =============================================================

modifier onlyTokenManager() {
if (msg.sender != tokenManager) revert OnlyTokenManager();
_;
}

// =============================================================
// EXTERNAL FUNCTIONS
// =============================================================

/// @notice Initialize the ERC20 token
/// @param name_ The token name
/// @param symbol_ The token symbol
/// @param cap_ The maximum supply cap (in 18 decimals)
/// @param deployTxHash_ The ethscription hash that deployed this token
function initialize(
string memory name_,
string memory symbol_,
uint256 cap_,
bytes32 deployTxHash_
) public initializer {
) external initializer {
__ERC20_init(name_, symbol_);
__ERC20Capped_init(cap_);
deployTxHash = deployTxHash_;
}

modifier onlyTokenManager() {
require(msg.sender == tokenManager, "Only TokenManager");
_;
}

// TokenManager-only mint that allows to == address(0)
/// @notice Mint tokens (TokenManager only)
/// @dev Allows minting to address(0) for null ownership
/// @param to The recipient address (can be address(0))
/// @param amount The amount to mint (in 18 decimals)
function mint(address to, uint256 amount) external onlyTokenManager {
_mint(to, amount);
}

// TokenManager-only transfer that allows to/from == address(0)
/// @notice Force transfer tokens (TokenManager only)
/// @dev Allows transfers to/from address(0) for null ownership
/// @param from The sender address (can be address(0))
/// @param to The recipient address (can be address(0))
/// @param amount The amount to transfer (in 18 decimals)
function forceTransfer(address from, address to, uint256 amount) external onlyTokenManager {
_update(from, to, amount);
}

// Disable user-initiated ERC20 flows
// =============================================================
// DISABLED ERC20 FUNCTIONS
// =============================================================

/// @notice User-initiated transfers are disabled
/// @dev All transfers must go through the Ethscriptions NFT
function transfer(address, uint256) public pure override returns (bool) {
revert("Transfers only allowed via Ethscriptions NFT");
revert TransfersOnlyViaEthscriptions();
}

/// @notice User-initiated transfers are disabled
/// @dev All transfers must go through the Ethscriptions NFT
function transferFrom(address, address, uint256) public pure override returns (bool) {
revert("Transfers only allowed via Ethscriptions NFT");
revert TransfersOnlyViaEthscriptions();
}

/// @notice Approvals are disabled
/// @dev All transfers are controlled by the TokenManager
function approve(address, uint256) public pure override returns (bool) {
revert("Approvals not allowed");
revert ApprovalsNotAllowed();
}
}
}
73 changes: 52 additions & 21 deletions contracts/src/EthscriptionsProver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet
contract EthscriptionsProver {
using EnumerableSet for EnumerableSet.Bytes32Set;

// =============================================================
// STRUCTS
// =============================================================

/// @notice Info stored when an ethscription is queued for proving
struct QueuedProof {
uint256 l2BlockNumber;
Expand All @@ -21,21 +25,6 @@ contract EthscriptionsProver {
uint256 l1BlockNumber;
}

/// @notice Set of all ethscription transaction hashes queued for proving
EnumerableSet.Bytes32Set private queuedEthscriptions;

/// @notice Mapping from ethscription tx hash to its queued proof info
mapping(bytes32 => QueuedProof) private queuedProofInfo;

/// @notice L1Block contract address for access control
address public constant L1_BLOCK = Predeploys.L1_BLOCK_ATTRIBUTES;
/// @notice L2ToL1MessagePasser predeploy address on OP Stack
L2ToL1MessagePasser constant L2_TO_L1_MESSAGE_PASSER =
L2ToL1MessagePasser(Predeploys.L2_TO_L1_MESSAGE_PASSER);

/// @notice The Ethscriptions contract (pre-deployed at known address)
Ethscriptions public constant ethscriptions = Ethscriptions(Predeploys.ETHSCRIPTIONS);

/// @notice Struct for ethscription data proof
struct EthscriptionDataProof {
bytes32 ethscriptionTxHash;
Expand All @@ -51,19 +40,58 @@ contract EthscriptionsProver {
uint256 l2BlockNumber;
uint256 l2Timestamp;
}

/// @notice Events for tracking proofs

// =============================================================
// CONSTANTS
// =============================================================

/// @notice L1Block contract address for access control
address constant L1_BLOCK = Predeploys.L1_BLOCK_ATTRIBUTES;

/// @notice L2ToL1MessagePasser predeploy address on OP Stack
L2ToL1MessagePasser constant L2_TO_L1_MESSAGE_PASSER =
L2ToL1MessagePasser(Predeploys.L2_TO_L1_MESSAGE_PASSER);

/// @notice The Ethscriptions contract (pre-deployed at known address)
Ethscriptions constant ethscriptions = Ethscriptions(Predeploys.ETHSCRIPTIONS);

// =============================================================
// STATE VARIABLES
// =============================================================

/// @notice Set of all ethscription transaction hashes queued for proving
EnumerableSet.Bytes32Set private queuedEthscriptions;

/// @notice Mapping from ethscription tx hash to its queued proof info
mapping(bytes32 => QueuedProof) private queuedProofInfo;

// =============================================================
// CUSTOM ERRORS
// =============================================================

error OnlyEthscriptions();
error OnlyL1Block();

// =============================================================
// EVENTS
// =============================================================

/// @notice Emitted when an ethscription data proof is sent to L1
event EthscriptionDataProofSent(
bytes32 indexed ethscriptionTxHash,
uint256 indexed l2BlockNumber,
uint256 l2Timestamp
);

// =============================================================
// EXTERNAL FUNCTIONS
// =============================================================

/// @notice Queue an ethscription for proving
/// @dev Only callable by the Ethscriptions contract
/// @param txHash The transaction hash of the ethscription
function queueEthscription(bytes32 txHash) external virtual {
require(msg.sender == address(ethscriptions), "Only Ethscriptions contract can queue");
if (msg.sender != address(ethscriptions)) revert OnlyEthscriptions();

// Add to the set (deduplicates automatically)
if (queuedEthscriptions.add(txHash)) {
Expand All @@ -82,7 +110,7 @@ contract EthscriptionsProver {
/// @notice Flush all queued proofs
/// @dev Only callable by the L1Block contract at the start of each new block
function flushAllProofs() external {
require(msg.sender == L1_BLOCK, "Only L1Block can flush");
if (msg.sender != L1_BLOCK) revert OnlyL1Block();

uint256 count = queuedEthscriptions.length();

Expand All @@ -100,13 +128,17 @@ contract EthscriptionsProver {
}
}

// =============================================================
// INTERNAL FUNCTIONS
// =============================================================

/// @notice Internal function to create and send proof for an ethscription
/// @param ethscriptionTxHash The transaction hash of the ethscription
/// @param proofInfo The queued proof info containing block data
function _createAndSendProof(bytes32 ethscriptionTxHash, QueuedProof memory proofInfo) internal {
// Get ethscription data including previous owner
Ethscriptions.Ethscription memory etsc = ethscriptions.getEthscription(ethscriptionTxHash);
address currentOwner = ethscriptions.currentOwner(ethscriptionTxHash);
address currentOwner = ethscriptions.ownerOf(ethscriptionTxHash);

// Create proof struct with all ethscription data
EthscriptionDataProof memory proof = EthscriptionDataProof({
Expand All @@ -130,5 +162,4 @@ contract EthscriptionsProver {

emit EthscriptionDataProofSent(ethscriptionTxHash, proofInfo.l2BlockNumber, proofInfo.l2BlockTimestamp);
}

}
2 changes: 1 addition & 1 deletion contracts/src/L2/L1Block.sol
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ contract L1Block {
}

function _flushProofsIfLive() internal {
if (block.timestamp >= 1760630077) {
if (block.timestamp >= Constants.historicalBackfillApproxDoneAt) {
// Each proof includes its own block number and timestamp from when it was queued
IEthscriptionsProver(Predeploys.ETHSCRIPTIONS_PROVER).flushAllProofs();
}
Expand Down
Loading