# Depository Contract

The **DripToolsDepository** is the on-chain half of a swap on the **EVM** FROM chains. It records
that a payer paid a certain amount for a `quoteId` and emits a `Paid` event; everything else —
recipients, TO chain, TO amounts — lives off-chain in the [signed quote](https://docs.drip.tools/api/quote.md). This keeps
the on-chain call tiny and identical whether you swap to one recipient or thousands.

> **Info: Solana and TON have no contract**
>
> On the Solana and TON FROM chains the payment is a plain transfer to a treasury account with the
> `quoteId` as memo / comment. See [Paying from Solana & TON](https://docs.drip.tools/swap/memo.md). The
> [quote signature](https://docs.drip.tools/swap/contract.md#verifying-the-quote-signature-optional) described below is the same on every
> FROM chain.

## Addresses

There is **one pay target per FROM (mainnet) chain**. Never hardcode it — read it at runtime:

- From [`GET /api/chains`](https://docs.drip.tools/api/discovery.md#get-api-chains): `chain.contracts.depository`.
- From the [quote response](https://docs.drip.tools/api/quote.md): the `vault` field is the same address, lowercased —
  compare case-insensitively.

If a FROM chain has no `contracts.depository`, programmatic swaps aren't enabled there yet.

## Functions

All four payment functions take the `quoteId` and `deadline` from your quote. The paid amount must
equal the quote's `fromTotal`, and the transaction must land before `deadline`.

| Function | For | Notes |
| --- | --- | --- |
| `pay(bytes32 quoteId, uint256 deadline)` | Native FROM token | `payable`; `msg.value == fromTotal`. |
| `payWithToken(address token, uint256 amount, bytes32 quoteId, uint256 deadline)` | ERC-20 | Requires a prior `approve(payTarget, amount)`; `amount == fromTotal`. |
| `payWithTokenPermit(address token, uint256 amount, bytes32 quoteId, uint256 deadline, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s)` | ERC-20 with EIP-2612 | No approve tx — the permit signature authorizes the pull in the same call. A permit that fails is ignored; the pull then relies on an existing allowance. |
| `payWithPermit2(address token, uint256 amount, bytes32 quoteId, uint256 deadline, uint256 permit2Nonce, uint256 permit2Deadline, bytes permit2Signature)` | Any ERC-20 via Uniswap Permit2 | Uses a Permit2 allowance signature instead of EIP-2612. |

`token` must be the same FROM token you quoted (`quote.fromToken`). A fee-on-transfer token that
delivers less than `amount` reverts with `TokenTransferMismatch`; an expired `deadline` reverts
with `Expired`.

> **Warning: The contract does not know `fromTotal`**
>
> The contract only checks a per-token minimum. Paying a different amount than the quote's
> `fromTotal` succeeds on-chain but is **not** honoured off-chain — the payment is held for
> operator review instead of becoming an order. Always pass exactly `fromTotal`.

## ABI

Minimal ABI for the payment path (viem `parseAbi` form):

**JavaScript · viem**
```js
const depositoryAbi = parseAbi([
  'function pay(bytes32 quoteId, uint256 deadline) payable',
  'function payWithToken(address token, uint256 amount, bytes32 quoteId, uint256 deadline)',
  'function payWithTokenPermit(address token, uint256 amount, bytes32 quoteId, uint256 deadline, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s)',
  'function payWithPermit2(address token, uint256 amount, bytes32 quoteId, uint256 deadline, uint256 permit2Nonce, uint256 permit2Deadline, bytes permit2Signature)',
  'event Paid(address indexed payer, address indexed fromToken, uint256 fromAmount, bytes32 indexed quoteId)',
])
```

## Reverts you can hit

The contract uses custom errors — decode a revert against this list:

| Error | Meaning |
| --- | --- |
| `Expired` | The transaction landed after `deadline`. Request a fresh quote. |
| `MissingQuote` | `quoteId` is zero — you passed an empty quote id. |
| `AmountTooLow` / `ZeroAmount` | The paid amount is zero or below the contract's per-token floor. |
| `TokenTransferMismatch` | The token amount received ≠ `amount` (fee-on-transfer / rebasing tokens are not supported). |
| `TokenNotAllowed` | The FROM token isn't accepted by this contract — use one listed in `/api/chains`. |
| `DirectPaymentNotAllowed` | A plain ETH transfer to the contract (no `pay` call) was rejected. |
| `EnforcedPause` | The contract is paused by the operator. Try later. |

## Verifying the quote signature (optional)

The quote returned by [`POST /api/quote`](https://docs.drip.tools/api/quote.md) is signed with EIP-712, scheme version
**3**, by the address in `signer`. You don't have to check it — the backend does — but you can,
to make sure a quote really came from Drip Tools before spending.

**Domain**

**EIP-712 domain**
```js
{
  name: 'drip.tools quote',
  version: '3',
  chainId: <FROM chainId>,             // number — the cross-chain replay separator
}
```

There is **no `verifyingContract`** in the domain: the pay target is signed as the `depository`
message field instead, because a Solana or TON treasury does not fit a 20-byte address slot.
The binding is the same — a quote is tied to exactly one (FROM chain, depository) pair and
cannot be replayed against another chain or deployment.

**Types**

**EIP-712 types**
```js
Quote: [
  { name: 'quoteId',    type: 'bytes32'   },
  { name: 'depository', type: 'string'    },  // == quote.vault
  { name: 'sender',     type: 'string'    },
  { name: 'fromToken',  type: 'string'    },
  { name: 'fromTotal',  type: 'uint256'   },
  { name: 'toChainId',  type: 'uint256'   },
  { name: 'toToken',    type: 'string'    },
  { name: 'recipients', type: 'string[]'  },
  { name: 'toAmounts',  type: 'uint256[]' },
  { name: 'issuedAt',   type: 'uint256'   },
  { name: 'expiry',     type: 'uint256'   },
]
```

**Canonical string forms.** Every `string` field is signed in the form the API derived from your
request, so reproduce it exactly when verifying:

| Field | Canonical form |
| --- | --- |
| `depository`, `sender`, `fromToken` | Per the **FROM** chain's family. EVM: lowercased `0x…`. Solana: the base58 key exactly as sent. TON: the non-bounceable, url-safe `UQ…` form. The native sentinel `0x000…0` stays as is on every family. |
| `toToken` | Lowercased `0x…` (every TO chain; `0x000…0` for native). |
| `recipients[]` | Per the **TO** chain's family: EVM lowercased; TON, Solana and Bitcoin addresses exactly as sent (trimmed). |

`depository` and `fromToken` come back in canonical form on the quote (`vault`, `fromToken`);
`sender` and `recipients` are yours to canonicalize.

**Worked example** (viem):

**JavaScript · viem**
```js
import { verifyTypedData } from 'viem'

const ok = await verifyTypedData({
  address: quote.signer,
  domain: { name: 'drip.tools quote', version: '3', chainId: FROM_CHAIN_ID },
  types: {
    Quote: [
      { name: 'quoteId',    type: 'bytes32'   },
      { name: 'depository', type: 'string'    },
      { name: 'sender',     type: 'string'    },
      { name: 'fromToken',  type: 'string'    },
      { name: 'fromTotal',  type: 'uint256'   },
      { name: 'toChainId',  type: 'uint256'   },
      { name: 'toToken',    type: 'string'    },
      { name: 'recipients', type: 'string[]'  },
      { name: 'toAmounts',  type: 'uint256[]' },
      { name: 'issuedAt',   type: 'uint256'   },
      { name: 'expiry',     type: 'uint256'   },
    ],
  },
  primaryType: 'Quote',
  message: {
    quoteId:    quote.quoteId,
    depository: quote.vault,                    // as returned
    sender:     account.address.toLowerCase(),  // EVM: lowercased; Solana: as sent; TON: UQ… form
    fromToken:  quote.fromToken,                // as returned
    fromTotal:  BigInt(quote.fromTotal),
    toChainId:  BigInt(TO_CHAIN_ID),
    toToken:    TO_TOKEN.toLowerCase(),
    recipients: [RECIPIENT.toLowerCase()],      // EVM recipients lowercased; TON/Solana/BTC as sent
    toAmounts:  quote.toAmounts.map(BigInt),
    issuedAt:   BigInt(quote.issuedAt),
    expiry:     BigInt(quote.expiry),
  },
  signature: quote.signature,
})
if (!ok) throw new Error('Quote signature does not verify — do not pay')
```
