Smart contracts can’t see the future. A flash loan needs liquidity in a pool. A payment gate needs msg.value. An exit needs… patience.
Unless the contract can demand that the future happen.
Signet’s Orders system inverts this. Contracts specify what they need to receive, and fillers compete to provide it. The contract doesn’t care where the liquidity comes from—only that it arrives, atomically, in the same block.
The Pattern
Every example follows the same structure: specify outputs (what you need), specify inputs (what you’ll give), let the market solve it.
ORDERS.initiate(
block.timestamp, // deadline (same block = now)
inputs, // what you're giving
outputs // what you need to receive
);If outputs aren’t filled, the transaction reverts. No partial execution. No stranded assets.
The signet-sol repository provides base contracts that auto-configure across networks:
contract SignetL2 {
constructor() {
if (block.chainid == 88888) { // Parmigiana
HOST_CHAIN_ID = 3151908;
PASSAGE = 0x0000000000007369676e65742d70617373616765;
ORDERS = 0x000000000000007369676e65742d6f7264657273;
}
}
}Deploy identical bytecode everywhere. Chain detection handles the rest.
Flash Loans Without Pools
Traditional flash loans require liquidity pools—capital sitting idle, waiting to be borrowed. Signet’s approach: demand the capital, let fillers provide it.
abstract contract Flash is SignetL2 {
modifier flash(address asset, uint256 amount) {
_; // Your logic executes here
// Output: what we received at start of tx
RollupOrders.Output[] memory outputs = new RollupOrders.Output[](1);
outputs[0] = makeRollupOutput(asset, amount, address(this));
// Input: what we return at end of tx
RollupOrders.Input[] memory inputs = new RollupOrders.Input[](1);
inputs[0] = makeInput(asset, amount);
ORDERS.initiate(block.timestamp, inputs, outputs);
}
}The flash modifier receives assets before your function executes, then returns them after. Fillers provide just-in-time liquidity from their own inventory—no pool required.
Why this matters: Capital efficiency. Traditional pools lock capital that earns nothing until borrowed. Fillers deploy capital only when profitable, then immediately redeploy elsewhere.
Payment Gates Without msg.value
What if you could require payment without handling funds directly? Demand that someone pays you, and refuse to execute unless they do.
abstract contract PayMe is SignetL2 {
modifier payMe(uint256 amount) {
_;
demandPayment(NATIVE_ASSET, amount);
}
function demandPayment(address asset, uint256 amount) internal {
RollupOrders.Output[] memory outputs = new RollupOrders.Output[](1);
outputs[0] = makeRollupOutput(asset, amount, address(this));
ORDERS.initiate(
block.timestamp,
new RollupOrders.Input[](0), // no inputs—pure demand
outputs
);
}
}The payMe modifier demands payment as an output. No inputs means you’re not offering anything in exchange—just requiring that funds arrive. The filler pays you to unlock whatever value your function provides.
Gas-subsidized variant: Calculate gas cost and subtract from demanded payment:
modifier payMeSubsidizedGas(uint256 amount) {
uint256 pre = gasleft();
uint256 gp = tx.gasprice;
_;
uint256 post = gasleft();
uint256 loot = amount - (gp * (pre - post));
demandPayment(NATIVE_ASSET, loot);
}Execution Bounties
The inverse of payment gates: offer rewards for executing functions.
abstract contract PayYou is SignetL2 {
modifier paysYou(uint256 tip) {
_;
providePayment(NATIVE_ASSET, tip);
}
function providePayment(address asset, uint256 amount) internal {
RollupOrders.Input[] memory inputs = new RollupOrders.Input[](1);
inputs[0] = makeInput(asset, amount);
ORDERS.initiate{value: amount}(
block.timestamp,
inputs,
new RollupOrders.Output[](0) // no outputs—pure bounty
);
}
}No outputs means you’re offering payment without requiring anything in return. Fillers compete to call your function and claim the bounty.
Use case: Automated execution. Post a bounty on a keeper function. Fillers call it when profitable, creating a decentralized scheduling system.
Instant Exits
Most rollups make you wait days to exit. Signet: post an order, get L1 liquidity instantly.
contract GetOut is SignetL2 {
receive() external payable {
getOut();
}
function getOut() public payable {
require(msg.value > 0);
uint256 desired = msg.value * 995 / 1000; // 0.5% fee
RollupOrders.Input[] memory inputs = new RollupOrders.Input[](1);
inputs[0] = makeInput(NATIVE_ASSET, msg.value);
RollupOrders.Output[] memory outputs = new RollupOrders.Output[](1);
outputs[0] = hostUsdcOutput(desired, msg.sender);
ORDERS.initiate{value: msg.value}(block.timestamp, inputs, outputs);
}
}User sends USD on Signet, receives USDC on Ethereum in the same block. The 0.5% spread goes to the filler who provides L1 liquidity.
The economics: Fillers hold inventory on both chains. They accept your Signet USD (which they can use or exit later) and send you their L1 USDC immediately. Market competition drives spreads toward equilibrium.
Why This Works
Traditional cross-chain execution is imperative: call this contract, wait for this bridge, claim from this pool. Signet’s Orders are declarative: specify what you need, let the market provide it.
This inverts MEV dynamics. Instead of extractors front-running your trades, fillers compete to serve you. The same actors, different incentives.
Synchronous composability: All execution happens in one block. Flash loans that span chains. Arbitrage that settles atomically. Applications that treat cross-chain like local calls.
Application-directed MEV: Your contracts define the terms. Fillers accept them or don’t. Value capture becomes intentional, not leaked.
Get Started
- signet-sol — All contract patterns
- Parmigiana Testnet — Deploy and test
- Orders Documentation — Technical specs
- The Filler Economy — How markets work
Questions? Get in touch.