PUSH4 Protocol
View on GitHubTransform 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.
// SPDX-License-Identifier: MITpragma 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 herer = 255 - r; // Example: invert redreturn 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).
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.
// SPDX-License-Identifier: MITpragma 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 selectoruint8 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 halfif (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 tableif (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 valuereturn 0; // Fallback (should not happen with valid selectors)}}
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 formulauint8 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/push4cd push4bun install
Create Your Proxy
Add a new contract to src/:
// src/MyProxy.solpragma solidity ^0.8.0;contract MyProxy {function execute(bytes4 selector) external pure returns (bytes4) {// Your transformation logic herereturn selector;}}
Deploy
# Sepolia (testnet)forge create src/MyProxy.sol:MyProxy \--rpc-url sepolia \--private-key $PRIVATE_KEY# Mainnetforge 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/web3const 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.
execute() under 500 gas per call to ensure efficient rendering.