Skip to content

Instantly share code, notes, and snippets.

@recmo
Last active November 1, 2024 14:51
Show Gist options
  • Save recmo/8adf1b518e17bb1d11b9cf565058714f to your computer and use it in GitHub Desktop.
Save recmo/8adf1b518e17bb1d11b9cf565058714f to your computer and use it in GitHub Desktop.
object "Claim" {
code {
datacopy(0, dataoffset("Runtime"), datasize("Runtime"))
return(0, datasize("Runtime"))
}
object "Runtime" {
code {
// Memory layout
// 0..192: ModExp precompile calldata
// 96..128: base
// 128..160: exponent (defaults to (p+1)/4, i.e. sqrt)
// 160..192: modulus (set to p)
// 192..580: Claim calldata
// 196..228: grantId (fixed for batch)
// 228..260: receiver
// 260..292: root (fixed for batch)
// 292..324: nullifierHash
// 324..580: proof
// Set modexp calldata and claim bytes4
datacopy(0, dataoffset("Template"), datasize("Template"))
// Shared per batch
// 1 byte: grantId
// 32 bytes: root
// mstore(196, add(2560, shr(248, calldataload(0))))
mstore(196, shr(248, calldataload(0)))
mstore(260, calldataload(1))
// Loop through batch
let offset := 33
for { } lt(offset, calldatasize()) { offset := add(offset, 180) } {
// 20 bytes: receiver address
// 32 bytes: nullifierHash
// 32 bytes: A x
// 32 bytes: B x 0
// 32 bytes: B x 1
// 32 bytes: C x
// Total 180 bytes.
// Set recipient address.
mstore(228, shr(96, calldataload(offset)))
// Set nullifierHash
mstore(292, calldataload(add(offset, 20)))
// Proof
{ // A
let x, y := decompress_g1(calldataload(add(offset, 52)))
mstore(324, x)
mstore(356, y)
}
{ // C
let x, y := decompress_g1(calldataload(add(offset, 148)))
mstore(516, x)
mstore(548, y)
}
{ // B
let x0, x1, y0, y1 := decompress_g2(
calldataload(add(offset, 84)),
calldataload(add(offset, 116))
)
mstore(388, x1)
mstore(420, x0)
mstore(452, y1)
mstore(484, y0)
}
pop(call(gas(), 0xe773335550b63eed23a6e60dcc4709106a1f653c, 0, 192, 388, 0, 0))
// pop(call(gas(), 0xC84337376696EfdefE38CD6112b4ad70E1EFCA95, 0, 192, 388, 0, 0))
// Continue if call failed. We don't want to revert the whole batch.
}
function decompress_g1(n) -> x, y {
let P := 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47
x := shr(1, n)
// Compute x³ + 3 which equals y²
// Note: Sqrt in the next step takes care of final reduction.
mstore(96, add(3, mulmod(mulmod(x, x, P), x, P)))
// y = sqrt y²
// Note: If the input does not have a quadratic residue it instead
// returns the quadratic residue of the negated input.
pop(staticcall(gas(), 5, 0, 192, 96, 32))
y := mload(96)
// Negate y if bit is set
// Note: This does not reduce correctly for 0, but no point on the
// curve has a zero y-coordinate since (x³ + 3) is irreducible.
if and(n, 1) {
y := sub(P, y)
}
}
function decompress_g2(n0, n1) -> x0, x1, y0, y1 {
let P := 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47
x0 := shr(2, n0)
x1 := n1
// -3⋅x₀⋅x₁
let n3ab := mulmod(mulmod(x0, x1, P),
0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd44
, P)
// a₀ = 27/82 + x₀³ - 3⋅x₀⋅x₁²
let a0 := addmod(
0x2b149d40ceb8aaae81be18991be06ac3b5b4c5e559dbefa33267e6dc24a138e5,
addmod(
mulmod(mulmod(x0, x0, P), x0, P),
mulmod(n3ab, x1, P), P), P)
// a₁ = -(3/82 + x₁³ - 3 ⋅ x₀²⋅x₁)
let a1 := sub(P, addmod(
0x2fcd3ac2a640a154eb23960892a85a68f031ca0c8344b23a577dcf1052b9e775,
addmod(
mulmod(mulmod(x1, x1, P), x1, P),
mulmod(n3ab, x0, P), P), P))
// d = ± sqrt(a₁² + a₁²)
mstore(96, addmod(mulmod(a0, a0, P), mulmod(a1, a1, P), P))
pop(staticcall(gas(), 5, 0, 192, 96, 32))
let d := mload(96)
// Note: The next addmod takes care of the reduction if d is zero.
if and(n0, 1) {
d := sub(P, d)
}
// y₀ = sqrt((a₀ + d) / 2)
mstore(96, mulmod(
0x183227397098d014dc2822db40c0ac2ecbc0b548b438e5469e10460b6c3e7ea4,
addmod(a0, d, P), P))
pop(staticcall(gas(), 5, 0, 192, 96, 32))
y0 := mload(96)
// y₁ = a₁ / (2⋅y₀)
// Note: we temporarily set the exponent to p-2 to compute the inverse.
// OPT: Use a batch inversion for the whole claims batch.
mstore(96, add(y0, y0)) // modexp will reduce
mstore(128, 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd45)
pop(staticcall(gas(), 5, 0, 192, 96, 32))
mstore(128, 0x0c19139cb84c680a6e14116da060561765e05aa45a1c72a34f082305b61f3f52)
y1 := mulmod(a1, mload(96), P)
// Note: Points with y0 = 0 or y1 = 0 should not set the negate bit.
if and(n0, 2) {
y0 := sub(P, y0)
y1 := sub(P, y1)
}
}
}
data "Template" hex"00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000c19139cb84c680a6e14116da060561765e05aa45a1c72a34f082305b61f3f5230644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47a41e6ceb00000000000000000000000000000000000000000000000000000000"
}
}
/* Example uncompressed transaction
https://optimistic.etherscan.io/tx/0x2ec43c5d9c1a6a9d887534c33e5eb7a174eb0a0f6c7b6fc4c0fff3ba2096c676
execTransaction(
address to,
uint256 value,
bytes calldata data,
Enum.Operation operation,
uint256 safeTxGas,
uint256 baseGas,
uint256 gasPrice,
address gasToken,
address payable refundReceiver,
bytes memory signatures
)
000 6a761202 selector
004 000000000000000000000000b5fbfeba9848664fd1a49dc2a250d9b5d1294f2a to (ApprovalSwapRouter)
036 0000000000000000000000000000000000000000000000000000000000000000 value
068 0000000000000000000000000000000000000000000000000000000000000140 data offset
100 0000000000000000000000000000000000000000000000000000000000000001 operation
132 0000000000000000000000000000000000000000000000000000000000030d40 safeTxGas
164 0000000000000000000000000000000000000000000000000000000000000000 baseGas
196 0000000000000000000000000000000000000000000000000000000000000000 gasPrice
228 0000000000000000000000000000000000000000000000000000000000000000 gasToken
260 0000000000000000000000000000000000000000000000000000000000000000 refundReceiver
292 0000000000000000000000000000000000000000000000000000000000000280 signatures offset
// data
exactInputSingle((
address tokenIn,
address tokenOut,
uint24 fee,
address recipient,
uint256 deadline,
uint256 amountIn,
uint256 amountOutMinimum,
uint160 sqrtPriceLimitX96
))
324 0000000000000000000000000000000000000000000000000000000000000104 length (260 bytes)
356 414bf389 selector
360 0000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c31607 tokenIn
392 00000000000000000000000068f180fcce6836688e9084f035309e29bf0a2095 tokenOut
424 00000000000000000000000000000000000000000000000000000000000001f4 fee
456 0000000000000000000000000bb48311c5e339d50c3bd6a2a8aa4eea964b2ebb recipient (== self)
488 0000000000000000000000000000000000000000000000000000000064c4e78a deadline
520 000000000000000000000000000000000000000000000000000000000007a120 amountIn
552 000000000000000000000000000000000000000000000000000000000000069a amountOutMinimum
584 0000000000000000000000000000000000000000000000000000000000000000 sqrtPriceLimitX96
// padding (28 bytes)
616 00000000000000000000000000000000000000000000000000000000
// signatures
644 0000000000000000000000000000000000000000000000000000000000000041 length (65 bytes)
676 4001b35fcfd1fe5770e2fe0aa35bf0edf8ae0d8b2bbff331d05fbc2bebb3f401 r
708 0f1dbac698c6b1528df3c01e2741f5f189feb275063971e987b4b12ca717666c s
740 1b v
// padding (31 bytes)
741 00000000000000000000000000000000000000000000000000000000000000
total 772 bytes
*/
object "SafeSwap" {
code {
datacopy(0, dataoffset("Runtime"), datasize("Runtime"))
return(0, datasize("Runtime"))
}
object "Runtime" {
code {
// Load template.
// Datacopy Costs 3 gas per word. Template is 22 words excluding
// trailing zeros. Total 66 gas.
// Alternatively, 7 pokes would suffice at 9 gas each. Total 63 gas.
// Hybrid approach: 12 word copy + 1 poke: 45 gas.
datacopy(0, dataoffset("Template"), datasize("Template"))
mstore(644, 0x41)
// Fixed values per batch
// 3 bytes: Deadline offset in 16 second increments starting from 1693937440.
// This allows dates until 2032-03-08.
let deadlineoffset := add(1693937440, shl(4, shr(232, calldataload(0))))
// Loop through batch
let offset := 3
for { } lt(offset, calldatasize()) { offset := add(offset, 117) } {
// 20 bytes: wallet address
// 1 byte : pool index (tokenIn, tokenOut, fee)
// 1 bytes: deadline - offset.
// 12 bytes: amountIn.
// 12 bytes: amountOutMinimum.
// 32 bytes: signature r
// 32 bytes: signature s & v
// Total 110 bytes.
// Set token pair and fee
datacopy(16, add(dataoffset("Tokens"), mul(shr(248, calldataload(add(offset, 21)), 192)), 192)
// Set recipient address to wallet address.
mstore(360, shr(96, calldataload(offset)))
// Set deadline
// 8 bits allows a batch to have deadlines spanning 4 minutes.
mstore(392, add(deadlineoffset, shr(248, calldataload(add(offset, 21)))))
// Set amountIn.
mstore(424, shr(160, calldataload(add(offset, 41))))
// Set amountOutMinimum.
mstore(456, shr(160, calldataload(add(offset, 41))))
// Set r.
mstore(484, calldataload(add(offset, 53)))
// Set v and s.
{
let s := calldataload(add(offset, 85))
mstore8(548, add(27, and(s, 1)))
mstore(516, shr(1, s))
}
let _ := call(gas(), shr(96, calldataload(offset)), 0, 0, 580, 0, 0)
// Continue if call failed. We don't want to revert the whole batch.
}
}
data "Template" hex"6a761202000000000000000000000000b5fbfeba9848664fd1a49dc2a250d9b5d1294f2a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000030d40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000104414bf389"
// Tuples of (tokenIn, tokenOut, poolFee) (3×32 bytes)
data "Pools" hex"000000000000000000000000dc6ff44d5d932cbd77b52e5612ba0529dc6226f10000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c31607000000000000000000000000000000000000000000000000000000000000271000000000000000000000000068f180fcce6836688e9084f035309e29bf0a20950000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c3160700000000000000000000000000000000000000000000000000000000000001f40000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c31607000000000000000000000000dc6ff44d5d932cbd77b52e5612ba0529dc6226f100000000000000000000000000000000000000000000000000000000000027100000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c3160700000000000000000000000068f180fcce6836688e9084f035309e29bf0a209500000000000000000000000000000000000000000000000000000000000001f400000000000000000000000042000000000000000000000000000000000000060000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c3160700000000000000000000000000000000000000000000000000000000000001f40000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c31607000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000001f40000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c31607000000000000000000000000dc6ff44d5d932cbd77b52e5612ba0529dc6226f10000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000dc6ff44d5d932cbd77b52e5612ba0529dc6226f10000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c316070000000000000000000000000000000000000000000000000000000000000bb8"
}
}
// TODO: Instead of address we could send the creation salt and tx
// nonce. Use ECRECOVER to get the owner and then use that to
// reconstruct the Safe deployment and create2 address.
// This costs at least 3k gas and saves ~14 bytes when using 3-byte salt/nonce.
object "SafeTransfer" {
code {
datacopy(0, dataoffset("Runtime"), datasize("Runtime"))
return(0, datasize("Runtime"))
}
object "Runtime" {
code {
// Load template. Costs 3 gas per word. Template is 16 words excluding
// trailing zeros. Total 48 gas.
// Alternatively, since it's mostly zeros, six pokes would suffice at
// 9 gas each. Total 54 gas.
datacopy(0, dataoffset("Template"), datasize("Template"))
// Loop through batch
let offset := 0
for { } lt(offset, calldatasize()) { offset := add(offset, 117) } {
// 20 bytes: wallet address
// 1 byte : token index.
// 20 bytes: to address
// 12 bytes: amount
// 32 bytes: signature r
// 32 bytes: signature s & v
// Total 117 bytes.
// Set token address.
datacopy(16, add(dataoffset("Tokens"), mul(
shr(248, calldataload(add(offset, 20)))
, 20)), 20)
// Set to address.
mstore(360, shr(96, calldataload(add(offset, 21))))
// Set amount.
mstore(392, shr(160, calldataload(add(offset, 41))))
// Set r.
mstore(484, calldataload(add(offset, 53)))
// Set v and s.
{
let s := calldataload(add(offset, 85))
mstore8(548, add(27, and(s, 1)))
mstore(516, shr(1, s))
}
let _ := call(gas(), shr(96, calldataload(offset)), 0, 0, 580, 0, 0)
// Continue if call failed. We don't want to revert the whole batch.
}
}
data "Template" hex"6a76120200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000044a9059cbb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041"
// TODO: We can have up to 256 tokens at no extra cost. Add all the popular ones!
data "Tokens" hex"dC6fF44d5d932Cbd77B52E5612Ba0529DC6226F142000000000000000000000000000000000000067F5c764cBc14f9669B88837ca1490cCa17c3160768f180fcCe6836688e9084f035309E29Bf0A2095"
// data "Tokens" hex"6FCc5ff6CD784b11D55e3bEBDD05376f7c17E6F64200000000000000000000000000000000000006f132e6112e358a36fCCF4660d082C9dAe3DA7411b21Fa419e6C82b6915Ab759507283b7b0F2Cb59E"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment