Skip to content

feat(arbitrage): add intra-Uniswap arbitrage mandate + agent scaffold - #7

Open
StSora wants to merge 3 commits into
intuition-box:mainfrom
StSora:main
Open

feat(arbitrage): add intra-Uniswap arbitrage mandate + agent scaffold#7
StSora wants to merge 3 commits into
intuition-box:mainfrom
StSora:main

Conversation

@StSora

@StSora StSora commented Jul 25, 2026

Copy link
Copy Markdown

What this adds

The arbitrage rail: one delegation that lets a redeem-only agent run
intra-Uniswap arbitrage on the Safe's treasury, bounded by an on-chain profit
floor. Non-custodial by construction.

  • src/lib/arbitrageMandate.tsbuildArbitrageMandate (complete)
  • scripts/arbitrage-agent.ts — runner scaffold (see status below)
  • docs/HOURGLASS_ARBITRAGE.md — full design doc

Draft: the agent is a scaffold. The mandate builder and the design are complete;
the execute composer and the mandate loader are intentional stubs (they throw).

Design

  • Delegator = Safe (via its DeleGator module); delegate = agent (redeem only). The
    swap executes as the Safe; funds never leave it.
  • functionCall scope, execute-only (no approve, no token target) +
    erc20BalanceChange(Increase) profit floor on the base token. Any redemption
    that does not leave the Safe ≥ minProfit richer reverts.
  • Approval is Permit2, done as two Safe setup txs outside the mandate:
    approve(PERMIT2, cap) then Permit2.approve(token, router, cap, expiration).
    The Universal Router pulls only via Permit2, so a legacy router approve is dead
    allowance. amount is the per-run size lever; expiration a free time bound.
  • Treasury protection is the approval surface (grant a Permit2 allowance for the
    base token only); a Decrease(0) caveat is unencodable.
  • Single execution, recipient = safeAddress (MSG_SENDER is the Safe).

Reconciled against the SDK + Uniswap's contracts

  • erc20BalanceChange rejects balance <= 0nminProfit = 0 unencodable (floor
    enforced at build), Decrease(0) impossible.
  • UNIVERSAL_ROUTER lives in src/config/uniswap.ts (mainnet + Base). Permit2 is
    net-new, resolved per chainId (zkSync differs). The Increase floor resolves to
    the SDK default ERC20BalanceChangeEnforcer.
  • msg.sender is the Safe (Safe-module call path), confirmed from the module
    bytecode — one on-chain confirmation still pending.

Pending (out of scope for this PR)

  • The execute composer (needs @uniswap/universal-router-sdk)
  • The signed-mandate loader
  • Testnet UNIVERSAL_ROUTER + Permit2 config entries
  • On-chain confirmation of msg.sender = Safe
  • Fork tests: profitable / below-floor / leg-2 fails / approve-only reverts /
    drain rejected / protected token

Note for the team (separate from this PR)

While writing this we found a defect in the DCA rail

StSora added 2 commits July 25, 2026 11:46
Add the arbitrage rail: one delegation that lets a redeem-only agent run
intra-Uniswap arbitrage on the Safe's treasury, bounded by an on-chain
profit floor. Non-custodial by construction.

Design (docs/HOURGLASS_ARBITRAGE.md):
- Delegator = Safe (via its DeleGator module), delegate = agent (redeem
  only). The swap executes as the Safe; funds never leave it.
- functionCall scope, execute-only (no approve, no token target) +
  erc20BalanceChange(Increase) profit floor on the base token. Any
  redemption that does not leave the Safe >= minProfit richer reverts.
- Approval is Permit2, done as two Safe setup txs OUTSIDE the mandate:
  approve(PERMIT2, cap) then Permit2.approve(token, router, cap,
  expiration). The Universal Router pulls funds only via Permit2, so a
  legacy router approve is dead allowance. The Permit2 amount is the
  per-run size lever; expiration is a free time bound.
- Treasury protection is the approval surface (grant a Permit2 allowance
  for the base token only); a Decrease(0) caveat is unencodable.
- Single execution, recipient = Safe (MSG_SENDER is the Safe), so the
  Increase before/after hooks wrap the whole round-trip.

Reconciled against the SDK and Uniswap's contracts:
- erc20BalanceChange rejects balance <= 0n, so minProfit = 0 is
  unencodable (floor enforced at build) and Decrease(0) is impossible.
- Universal Router lives in src/config/uniswap.ts (mainnet + Base);
  Permit2 is net-new, resolved per chainId. The Increase floor resolves
  to the SDK default ERC20BalanceChangeEnforcer.
- msg.sender is the Safe (Safe-module call path), confirmed from the
  module bytecode; one on-chain confirmation still pending.

Files:
- src/lib/arbitrageMandate.ts: buildArbitrageMandate (complete).
- scripts/arbitrage-agent.ts: runner SCAFFOLD — quotes, floor check,
  redeem wiring and circuit breaker done; the execute composer and the
  mandate loader are stubs that throw.
- docs/HOURGLASS_ARBITRAGE.md: full design doc.

Pending: the execute composer (needs @uniswap/universal-router-sdk),
the mandate loader, testnet Universal Router + Permit2 config, and an
on-chain confirmation of msg.sender = Safe.
@host-intuition-box

host-intuition-box Bot commented Jul 25, 2026

Copy link
Copy Markdown

The preview deployment for Hourglass is ready. 🟢

Open Preview | Open Build Logs | Open Application Logs

Last updated at: 2026-07-25 13:27:05 CET

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant