Skip to content
nikolas-sapaPublic

About

A vault contract on Monad that caps how much value can leave per day — lowering the cap is instant, raising it is timelocked. Unaudited testnet project.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

13 Commits

Folders and files

Repository files navigation

Curb

License: MIT Solidity PRs welcome

Your bank card has a daily spending limit. Your wallet doesn't. Curb gives it one.

A vault contract on Monad that holds your funds and caps how much can leave per day. Lowering the cap is instant. Raising it is timelocked. So one bad click — or one stolen key — can't take everything.

Live demo: (deploying — see Status below) Contract: (deploying) on Monad Testnet (chain 10143)

Unaudited. This is hackathon-stage code on Monad testnet. It has not been reviewed by an auditor. Do not use it with real funds. See SECURITY.md.

Quick start

git clone https://github.com/nikolas-sapa/curb && cd curb

# contracts
cd contracts && forge test          # 30 tests

# frontend
cd ../web && npm install
echo "NEXT_PUBLIC_CURB_ADDRESS=0x..." > .env.local
npm run dev                         # localhost:3000

The problem this solves, honestly

I'm not a crypto person. I have about $20 in a wallet I opened once and abandoned, because two things scared me off: the seed phrase, and the send button. Every wallet I opened asked me to paste a long string of gibberish and then hand over an amount with no ceiling and no way back.

My bank account has never scared me like that, and the reason is boring: my card has a daily limit. If someone gets my card, they get one day's limit, not my life savings. I didn't need to understand banking to be protected. The limit did the work.

Crypto wallets don't have that. Your key can always move everything, instantly. That's the whole gap Curb fills. It isn't a new idea — Argent v1 shipped a daily limit years ago (prior art, credited below). It's just a missing default, and this is what it looks like on a chain fast enough that using it doesn't feel like waiting.

How it works

One contract, one account per address.

  • open(cap, raiseDelay) — create your vault, optionally funding it in the same call.
  • send(to, amount) — move funds out, if it fits under your cap. Recipients get plain native MON: no claim step, no integration, works with any address or exchange.
  • lowerCap(newCap) — instant. Also abandons any pending raise.
  • requestRaise(newCap) → wait raiseDelay → applyRaise(). cancelRaise() is your veto.

The contract custodies the funds. That's the load-bearing detail: your key can't route around your own cap, because your key doesn't hold the money — spending goes through send() or it doesn't happen. Withdrawing to yourself is just a send(), so there's no uncapped back door.

Why this needs a contract, not an app setting: a limit your wallet enforces in its own UI is a suggestion — the key still signs anything. A limit the chain enforces is a rule. Nothing here relies on anyone reporting a real-world fact; the cap, the window, and the timelock are all chain-native state.

Threat model

Curb bounds the damage. It does not make you un-hackable, and it would be dishonest to imply otherwise. An attacker holding your key can still:

  • Spend up to your cap, every window, indefinitely.
  • Extract up to ~2× your cap in one burst by straddling a window boundary — the full cap just before the 24h reset, the full cap again just after. Windows are fixed, not a rolling sum. This is pinned by test_windowStraddleAllowsTwiceCap.
  • Request a raise. You have raiseDelay to notice and cancel it. If you're not watching, it lands. At the unlock moment, whether your cancelRaise or their applyRaise gets included first is an ordinary mempool race.
  • Set your cap to 0, freezing your account for a full raiseDelay even after you recover the key. Availability is deliberately traded for safety.

Your own exit is capped too. That's the product, not a bug: deposit only what you're content to withdraw at your chosen rate.

Other honest notes:

  • MIN_RAISE_DELAY is 60s so you can watch the full timelock loop in a demo. It is not a realistic security setting — use hours or a day. At 60s, validator timestamp drift is proportionally significant; at 24h it's noise.
  • raiseDelay is clamped to 365 days. Without a ceiling, a large value overflows block.timestamp + raiseDelay and bricks raises permanently.
  • Ether force-fed via selfdestruct isn't credited to any account and can't be withdrawn by anyone. It's a one-way sink; it only ever helps solvency.
  • Native MON only. No ERC-20 support — one feature that works beats five that don't.
  • Unaudited hackathon code on testnet. Don't put real money in it.

Deploy your own

cd contracts
cp ../.env.example ../.env          # add a funded testnet key
forge script script/Deploy.s.sol --rpc-url https://testnet-rpc.monad.xyz --broadcast

Testnet MON: https://faucet.monad.xyz

Why Monad

Honest answer: a spending limit doesn't require a fast chain. It works anywhere.

What Monad changes is whether anyone would use it. Curb adds a contract hop to every send. On a 12-second chain that's a tax you'd feel on every transaction, and a "safety feature" you feel is a safety feature you turn off. At 400ms blocks with eth_sendRawTransactionSync, the write returns its receipt in the same request — the UI shows you the measured confirmation time, and it's usually a few hundred milliseconds. The protection stops being something you notice.

Stack

Solidity + Foundry · Next.js 16 + React 19 · wagmi v3 + viem · Tailwind v4 · Geist

useWriteContractSync maps to eth_sendRawTransactionSync: receipt in the same request, no polling. The ABI is generated from the Foundry build output, so it can't drift from the deployed bytecode.

Prior art

Credit where it's due:

  • Argent v1 — daily transfer limits with guardian-based recovery. The direct ancestor of this idea.
  • Safe modules — allowance modules cover similar ground for multisigs.
  • EIP-7702 was the obvious tool here and it does not work for this. A delegated EOA keeps its key: delegated code runs when the account is called, not when the key signs a plain transaction, and the delegation can be cleared at will. A 7702 "spending limit" is a switch the attacker can flip off. Custody is what makes the cap real.

Contributing

Bug reports, PRs, and questions are welcome. See CONTRIBUTING.md for dev setup, the real forge/npm commands, and PR expectations. This project follows the Contributor Covenant.

Security

Curb is unaudited. Do not use it with real funds. If you find a vulnerability, do not open a public issue — email niksapa150@gmail.com. Full policy and known, accepted risks: SECURITY.md.

License

MIT, see LICENSE. Copyright (c) 2026 Nikolas Sapalidis.

About

A vault contract on Monad that caps how much value can leave per day — lowering the cap is instant, raising it is timelocked. Unaudited testnet project.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages