Deploying crosschain event composability (contests)
We showcase cross chain composability through the implementation of contests. Leveraging the same underlying mechanism powering TicTacToe, these contests can permissionlessly integrate with the events emitted by any contract in the Superchain.
See the frontend documentation (opens in a new tab) for how the contests UI is presented to the user.
How it works
Unlike TicTacToe which is deployed on every participating chain, the contests are deployed on a single L2, behaving like an application-specific OP Stack chain rather than a horizontally scaled app.
Implement contests
Contests.sol (opens in a new tab) contains the implementation of the contests. We won't go into the details of the implementation here, but instead focus on how the contests can leverage cross chain event reading to compose with other contracts in the Superchain.
Read cross-chain events
The system predeploy that enables pulling in validated cross-chain events is the CrossL2Inbox (opens in a new tab).
contract ICrossL2Inbox {
function validateMessage(Identifier calldata _id, bytes32 _msgHash) external view;
}
Create the contest
The two contest options are detailed below: BlockHash contest and TicTacToe contest.
BlockHash contest
With the existence of an event that emits the blockhash and height of a block, we can create a contest on the parity of the blockhash being even or odd.
contract BlockHashEmitter {
event BlockHash(uint256 blockHeight, bytes32 blockHash);
function emitBlockHash(uint256 _blockHeight) external {
bytes32 hash = blockhash(_blockHeight);
require(hash != bytes32(0));
emit BlockHash(_blockHeight, hash);
}
}
Integrating this emitter into a contest is extremely simple. The BlockHashContestFactory
is a simple factory that creates a new contest for a given chain and block height.
TicTacToe contest
A contest for TicTacToe is created on an accepted game between two players, captured by the emitted AcceptedGame
event. When decoding the event, the game is uniquely identified by the chain it was created on, chainId
, and the associated gameId
. These identifying properties of the game are used to create the resolver for the game.
contract TicTacToeContestFactory {
Contests public contests;
TicTacToe public tictactoe;
function newContest(Identifier calldata _id, bytes calldata _data) public payable {
// Validate Log
require(_id.origin == address(tictactoe), "not an event from the TicTacToe contract");
CrossL2Inbox(Predeploys.CROSS_L2_INBOX).validateMessage(_id, keccak256(_data));
bytes32 selector = abi.decode(_data[:32], (bytes32));
require(selector == TicTacToe.AcceptedGame.selector, "incorrect event");
// Decode the event data
(uint256 chainId, uint256 gameId, address creator,) = abi.decode(_data[32:], (uint256, uint256, address, address));
IContestResolver resolver = new TicTacToeGameResolver(contests, tictactoe, chainId, gameId, creator);
contests.newContest{ value: msg.value }(resolver, msg.sender);
}
}
Resolve contest
A contest is identified by and has its outcome determined by the IContestResolver
instance. The resolver starts in the UNDECIDED
state, updated into YES
or NO
when resolving itself
with the contest.
enum ContestOutcome {
UNDECIDED,
YES,
NO
}
interface IContestResolver {
function outcome() external returns (ContestOutcome);
}
Resolve BlockHash contest
When live, anyone can resolve the BlockHash contest by simply providing the right BlockHash
event to the deployed resolver.
contract BlockHashContestFactory {
Contests public contests;
BlockHashEmitter public emitter; // Same emitter deployed on every chain
function newContest(uint256 _chainId, uint256 _blockNumber) public payable {
IContestResolver resolver = new BlockHashResolver(contests, emitter, _chainId, _blockNumber);
contests.newContest{ value: msg.value }(resolver, msg.sender);
}
}
contract BlockHashResolver is IContestResolver {
Contests public contests;
ContestOutcome public outcome;
BlockHashEmitter public emitter;
// The target chain & block height
uint256 public chainId;
uint256 public blockNumber;
function resolve(Identifier calldata _id, bytes calldata _data) external {
require(outcome == ContestOutcome.UNDECIDED);
// Validate Log
require(_id.origin == address(emitter), "not an event from the emitter");
require(_id.chainId == chainId, "must match target chain");
CrossL2Inbox(Predeploys.CROSS_L2_INBOX).validateMessage(_id, keccak256(_data));
bytes32 selector = abi.decode(_data[:32], (bytes32));
require(selector == BlockHashEmitter.BlockHash.selector, "incorrect event");
// Event should correspond to the right contest
uint256 dataBlockNumber = abi.decode(_data[32:64], (uint256));
require(dataBlockNumber == blockNumber, "must match target block height");
// Resolve the contest (yes if odd, no if even)
bytes32 blockHash = abi.decode(_data[64:], (bytes32));
outcome = uint256(blockHash) % 2 != 0 ? ContestOutcome.YES : ContestOutcome.NO;
contests.resolveContest(this);
}
}
Resolve TicTacToe contest
When live, anyone can resolve the TicTacToe contest by providing the GameWon
or GameDraw
event of the associated game from the TicTacToe contract.
contract TicTacToeGameResolver is IContestResolver {
Contests public contests;
ContestOutcome public outcome;
TicTacToe public tictactoe;
// @notice Game for this resolver
Game public game;
constructor(Contests _contest, TicTacToe _tictactoe, uint256 _chainId, uint256 _gameId, address _creator) {
contests = _contest;
tictactoe = _tictactoe;
game = Game({chainId: _chainId, gameId: _gameId, creator: _creator});
outcome = ContestOutcome.UNDECIDED;
}
// @notice resolve this game by providing the game ending event
function resolve(Identifier calldata _id, bytes calldata _data) external {
// Validate Log
require(_id.origin == address(tictactoe));
CrossL2Inbox(Predeploys.CROSS_L2_INBOX).validateMessage(_id, keccak256(_data));
// Ensure this is a finalizing event
bytes32 selector = abi.decode(_data[:32], (bytes32));
require(selector == TicTacToe.GameWon.selector || selector == TicTacToe.GameDraw.selector, "event not a game outcome");
// Event should correspond to the right game
(uint256 _chainId, uint256 gameId, address winner,,) = abi.decode(_data[32:], (uint256, uint256, address, uint8, uint8));
require(_chainId == game.chainId && gameId == game.gameId);
// Resolve based on if the creator has won (non-draw)
outcome = winner == game.creator && selector != TicTacToe.GameDraw.selector ? ContestOutcome.YES : ContestOutcome.NO;
contests.resolveContest(this);
}
}
Takeaways
- Leveraging superchain interop, contracts in the superchain can compose with each other in a similar fashion to how they would on a single chain. No restrictions are placed on the kinds of events a contract can consume via the
CrossL2Inbox
. - In this example, the
BlockHashContestFactory
andTicTacToeContestFactory
can be seen as just starting points for theContests
app chain. As more contracts and apps are created in the superchain, developers can compose with them in a similar fashion without needing to change theContests
contract at all.