feat(arbitrage): add intra-Uniswap arbitrage mandate + agent scaffold - #7
Open
StSora wants to merge 3 commits into
Open
feat(arbitrage): add intra-Uniswap arbitrage mandate + agent scaffold#7StSora wants to merge 3 commits into
StSora wants to merge 3 commits into
Conversation
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.
|
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.ts—buildArbitrageMandate(complete)scripts/arbitrage-agent.ts— runner scaffold (see status below)docs/HOURGLASS_ARBITRAGE.md— full design docDesign
swap executes as the Safe; funds never leave it.
functionCallscope, execute-only (noapprove, no token target) +erc20BalanceChange(Increase)profit floor on the base token. Any redemptionthat does not leave the Safe ≥
minProfitricher reverts.approve(PERMIT2, cap)thenPermit2.approve(token, router, cap, expiration).The Universal Router pulls only via Permit2, so a legacy router approve is dead
allowance.
amountis the per-run size lever;expirationa free time bound.base token only); a
Decrease(0)caveat is unencodable.recipient = safeAddress(MSG_SENDERis the Safe).Reconciled against the SDK + Uniswap's contracts
erc20BalanceChangerejectsbalance <= 0n→minProfit = 0unencodable (floorenforced at build),
Decrease(0)impossible.UNIVERSAL_ROUTERlives insrc/config/uniswap.ts(mainnet + Base).Permit2isnet-new, resolved per
chainId(zkSync differs). The Increase floor resolves tothe SDK default
ERC20BalanceChangeEnforcer.msg.senderis the Safe (Safe-module call path), confirmed from the modulebytecode — one on-chain confirmation still pending.
Pending (out of scope for this PR)
executecomposer (needs@uniswap/universal-router-sdk)UNIVERSAL_ROUTER+Permit2config entriesmsg.sender = Safedrain rejected / protected token
Note for the team (separate from this PR)
While writing this we found a defect in the DCA rail