PUSH4 Studio

PUSH4 Protocol

View on GitHub

Transform the function selectors into new artworks by creating custom proxy contracts.

Quick Start

A proxy contract intercepts PUSH4 function calls and returns transformed bytes4 values. Each value encodes an RGB color and column position.

Minimal Proxy
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MyProxy {
function execute(bytes4 selector) external pure returns (bytes4) {
uint8 r = uint8(selector[0]);
uint8 g = uint8(selector[1]);
uint8 b = uint8(selector[2]);
uint8 col = uint8(selector[3]);
// Transform colors here
r = 255 - r; // Example: invert red
return bytes4(
bytes1(r) |
(bytes4(bytes1(g)) >> 8) |
(bytes4(bytes1(b)) >> 16) |
(bytes4(bytes1(col)) >> 24)
);
}
}

Selector Format

Each of PUSH4's 375 selectors encodes one pixel. The first 3 bytes are RGB values, the last byte is the column index (0-14).

4CR
34G
2EB
05Col
Column 5
bytes[0]Red (0-255)
bytes[1]Green (0-255)
bytes[2]Blue (0-255)
bytes[3]Column index (0-14)

Determining Row Position

Every selector is unique. While the column is encoded in bytes[3], the row position can be determined by looking up the selector in the original selector list. The renderer groups selectors by column, then sorts them by their numeric value to assign row positions (0-24).

This technique is used in PUSH4 Studio tools like Paint, Image, and AI to map selectors to their exact grid positions. You can use the same approach in your proxy contracts when you need both row and column data.

Row Lookup Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ProxyWithRowLookup {
function execute(bytes4 selector) external pure returns (bytes4) {
uint8 r = uint8(selector[0]);
uint8 g = uint8(selector[1]);
uint8 b = uint8(selector[2]);
uint8 col = uint8(selector[3]);
// Get the row position for this selector
uint8 row = getRenderRow(selector, col);
// Now you have both row and column!
// Use this to look up pixel data, apply row-based transformations, etc.
// Example: Darken pixels in the bottom half
if (row >= 12) {
r = r / 2;
g = g / 2;
b = b / 2;
}
return bytes4(
bytes1(r) |
(bytes4(bytes1(g)) >> 8) |
(bytes4(bytes1(b)) >> 16) |
(bytes4(bytes1(col)) >> 24)
);
}
function getRenderRow(bytes4 selector, uint8 col) internal pure returns (uint8) {
// For each column, selectors are sorted by their numeric value
// The sorted position becomes the row index (0-24)
// This is a partial example - see PUSH4Lib.sol for the complete lookup table
if (col == 0) {
if (selector == 0x4c392800) return 0;
if (selector == 0x46352900) return 1;
if (selector == 0x46393300) return 2;
if (selector == 0x4f302900) return 3;
if (selector == 0x51352d00) return 4;
if (selector == 0x4c2e3200) return 5;
if (selector == 0x50313000) return 6;
// ... 18 more selectors for column 0 (25 total, sorted by value)
if (selector == 0x502f2a00) return 24;
}
if (col == 1) {
if (selector == 0x472f2a01) return 0;
if (selector == 0x46392f01) return 1;
if (selector == 0x4e2e2b01) return 2;
// ... 22 more selectors for column 1
}
// ... repeat for all 15 columns (col 0-14)
// Each column has 25 selectors, sorted by their uint32 value
return 0; // Fallback (should not happen with valid selectors)
}
}
Full Implementation: The getRenderRow function performs a lookup by matching the selector against a sorted list of selectors for each column. In practice, you can generate this lookup table from the original 375 selectors by grouping by column and sorting by selector value. See the complete implementation in PUSH4Lib.sol for the full lookup table covering all 375 selectors across 15 columns.

Examples

Click to expand each example. All transformations happen in the execute function.

function execute(bytes4 selector) external pure returns (bytes4) {
uint8 r = uint8(selector[0]);
uint8 g = uint8(selector[1]);
uint8 b = uint8(selector[2]);
uint8 col = uint8(selector[3]);
// Luminance formula
uint8 gray = uint8(
(uint16(r) * 77 + uint16(g) * 150 + uint16(b) * 29) >> 8
);
return bytes4(
bytes1(gray) |
(bytes4(bytes1(gray)) >> 8) |
(bytes4(bytes1(gray)) >> 16) |
(bytes4(bytes1(col)) >> 24)
);
}

Deployment

Fork the repository and deploy your proxy with Foundry.

Clone & Setup

git clone https://github.com/ygtdmn/push4
cd push4
bun install

Create Your Proxy

Add a new contract to src/:

// src/MyProxy.sol
pragma solidity ^0.8.0;
contract MyProxy {
function execute(bytes4 selector) external pure returns (bytes4) {
// Your transformation logic here
return selector;
}
}

Deploy

# Sepolia (testnet)
forge create src/MyProxy.sol:MyProxy \
--rpc-url sepolia \
--private-key $PRIVATE_KEY
# Mainnet
forge create src/MyProxy.sol:MyProxy \
--rpc-url mainnet \
--private-key $PRIVATE_KEY

Register Your Proxy

After deploying, register your proxy with the PUSH4ProxyFactory so it appears in the Showcase page. The factory contract is deployed at:

0x996815bc3a8eb22ab254f2709b414b39a51e729e

Call the register function with your deployed proxy address:

# Using cast (from Foundry)
cast send 0x996815bc3a8eb22ab254f2709b414b39a51e729e \
"register(address)" \
YOUR_PROXY_ADDRESS \
--rpc-url mainnet \
--private-key $PRIVATE_KEY
# Or interact via ethers.js/web3
const factory = new ethers.Contract(
'0x996815bc3a8eb22ab254f2709b414b39a51e729e',
['function register(address proxy)'],
signer
);
await factory.register(YOUR_PROXY_ADDRESS);

Once registered, your proxy will appear in the Showcase page where anyone can view your transformation live.

Note: Each render makes 375 staticcalls to your proxy. It's recommended to keep execute() under 500 gas per call to ensure efficient rendering.