Skip to content
3F Docs

Settlement

Settlement in Grunt is the process by which bridge facilitator offers become funded bridge loans on-chain. This page explains who calls what, how the Request contract moves through funding and repayment, and how signed offers are verified.

For API-facing onboarding, address setup, and status tracking, start with Onboarding.

Every bridge loan is settled through a Request contract. The Facility deploys one Request per intent, and it serves as the settlement intermediary between bridge facilitators and the protocol.

The Request contract handles four concerns:

  1. Funding — accepting capital from bridge facilitators in exchange for PT/YT tokens
  2. Disbursement — allowing the Facility (the designated puller) to draw down the bridge loan
  3. Repayment — receiving repayment from the Facility once the underlying strategy settles
  4. Redemption — allowing PT/YT holders to redeem their tokens for the underlying asset
graph LR
    D[Deploy] --> F[Funding Phase] --> U[Fund Utilization] --> R[Repayment] --> RE[Redemption]
  1. Deploy — The RequestFactory creates a new Request along with its PT and YT vault contracts. The Request is initialized with the asset token, puller address, and repayment deadline.
  2. Funding phase — Bridge facilitators fund the Request through signed offer consumption or authorized push funding. PT and YT are minted as receipts.
  3. Fund utilization — The Facility calls pullFunds() to transfer bridge capital into the strategy.
  4. Repayment — The Facility returns funds through repay(), and the owner later marks the Request repaid via setRepaid().
  5. Redemption — PT and YT holders redeem against the assets now held by the Request.

Each bridge loan settles through a freshly-deployed Request contract, so the address you interact with is not predictable in advance. To let bridge facilitators confirm on-chain that a given address is a legitimate 3F-issued Request (and not a contract spoofed by a third party), 3F maintains an on-chain attestation registry: the Request Whitelist. See the Request Whitelist page for the full design rationale, governance model, and security properties; this section is the integration recipe.

Network Address
Ethereum mainnet 0x3FCD87948CBF46605d6dEd0eD56d3Daccd9DAF9e

Before approving funds to a Request, signing an offer for it, or accepting a consume() call from one, gate strictly on isWhitelisted(address) == Whitelisted. The registry returns one of four states; only Whitelisted is safe to authorise, every other value (including the two Paused* variants) means “do not proceed”.

You can integrate against the enum or directly against the underlying uint8.

With the enum:

enum WhitelistStatus {
NotWhitelisted,
Whitelisted,
PausedNotWhitelisted,
PausedWhitelisted
}
interface IWhitelist {
function isWhitelisted(address addr) external view returns (WhitelistStatus);
}
require(REQUEST_WHITELIST.isWhitelisted(request) == WhitelistStatus.Whitelisted);

Without the enum:

interface IWhitelist {
function isWhitelisted(address addr) external view returns (uint8);
}
require(REQUEST_WHITELIST.isWhitelisted(request) == 1);

The same call can be used from off-chain code (an eth_call against the registry) before signing an offer or sending an approval. Treat the registry’s output as the single source of truth — the registry’s pause state is reflected directly in the return value, so you do not need to read pause state separately.

Most operational confusion comes from role boundaries. The following matrix shows who acts at each stage:

Actor Controlled by Main responsibility Typical on-chain calls
BF identity You Account-level approval and integration identity No required settlement calls
BF operational funding address You Provides capital, receives PT/YT, and later redeems mint() in push-funded flows, token approvals, redemption
3F authorizer / consumer 3F Reflects accepted offers on-chain authorizeMinting(), consume()
Facility puller 3F Draws bridge capital into the strategy and later repays it pullFunds(), repay()
Request owner 3F Final state transitions and request-level administration setRepaid(), owner-gated admin actions

In the simplest setup, your BF identity and operational funding address are the same. In more advanced setups, the operational funding address can be a contract or multisig, as long as it can approve funds, receive PT/YT, and complete redemption later.

Once an offer is accepted, settlement follows a strict sequence:

graph LR
    A[Offer Accepted] --> B[consume or authorizeMinting]
    B --> C[Funds Enter Request]
    C --> D[PT/YT Minted]
    D --> E[pullFunds]
    E --> F[repay]
    F --> G[setRepaid]
    G --> H[PT/YT Redeemed]
  1. Request deployed — the Facility creates a Request for a specific strategy or intent.
  2. Offer accepted off-chain — the auction engine chooses a bridge facilitator offer.
  3. Funding path begins:
    • Signed pull path — a 3F operator calls consume() with the signed offer. The Request verifies the signature and pulls funds from the BF funding address.
    • Authorized push path — a 3F operator calls authorizeMinting(). The BF funding address then approves the asset and calls mint().
  4. PT/YT minted — the BF funding address receives PT and YT that represent principal and upside claims.
  5. Facility drawdown — the Facility calls pullFunds() to move the bridge capital into the strategy.
  6. Repayment — the Facility returns capital through repay() once the strategy settles.
  7. Redemption enabled — the Request owner calls setRepaid().
  8. BF redemption — the same BF funding address that received PT/YT redeems them for the underlying asset.

When a bridge facilitator signs an offer, it follows this on-chain structure:

struct Offer {
address maker; // Funding address that will receive PT/YT
uint256 amount; // Maximum principal the bridge facilitator will provide
uint256 expectedReturn; // Yield required on that principal
uint256 nonce; // Sequential nonce, must exceed stored minimum
uint256 expiration; // Unix timestamp after which the offer is void
bool useCallback; // Whether to call onRequestConsumed before pulling
}

The offer is signed as EIP-712 typed data against the specific Request contract’s domain. This binds the offer to a single Request and prevents replay across contracts or chains.

When consumed, the Request may fill the offer partially or fully. For a partial fill of ptAmount tokens, the corresponding YT amount is calculated proportionally:

ytAmount = offer.expectedReturn * ptAmount / offer.amount
Field or concept Meaning
maker The on-chain funding address that is tracked for nonce state and receives PT/YT.
deadline Endpoint-level authentication expiry for API typed-data auth. It is separate from settlement logic and should be short-lived.
expiration Offer-level validity inside the signed Offer message.
Request binding The EIP-712 domain verifyingContract and chainId tie the signature to one Request on one chain.
nonce Replay protection and cancellation state. An offer is valid only if its nonce is strictly greater than the stored minimum nonce for that address.
useCallback Commits the offer to just-in-time capital sourcing behavior before the Request pulls funds.

The auction engine is the off-chain component that coordinates offer collection and selection. It is not a smart contract; it is a service operated by 3F that manages the RFQ process.

  1. Collects offers — receives signed offers and API-managed offers from bridge facilitators.
  2. Evaluates pricing — selects the offers that provide the lowest cost of capital for the Facility.
  3. Executes settlement — either consumes signed offers or authorizes push-funded minting.
  4. Reports status — updates lifecycle status so bridge facilitators can track positions through the API.

The auction engine is a coordination layer. It does not hold bridge facilitator funds or signing authority over capital. The on-chain guarantees remain:

  • the Request contract verifies every signature independently
  • funds move through standard token transfer and approval rules
  • PT/YT minting is deterministic based on the accepted offer parameters
  • the auction engine cannot modify a signed offer after it has been produced

In many setups, the address that ultimately provides capital is not the same key that signs operational messages. For example:

  • a multisig treasury may control the funding address while a delegated operator signs requests
  • a smart contract vault may implement its own authorization logic for signing or callbacks

The Request contract supports this through EIP-1271 signature validation. When consuming an offer, the contract checks:

  1. If the maker is an EOA, it validates the signature using ecrecover.
  2. If the maker is a contract, it calls isValidSignature() on that contract.

This allows a bridge facilitator to use multisigs, vaults, or custom authorization contracts without changing the settlement model.

For API-managed push-funded offers, the flow is more explicit. A 3F operator calls authorizeMinting(funder, principalAmount, yieldAmount) for a specific BF funding address, and that funding address later calls mint() to transfer funds into the Request.

After mint(), the same funding address holds the PT/YT position and later performs redemption once the Request has been marked repaid.

Every signed offer includes a nonce field. Nonces serve two purposes: replay protection and offer cancellation.

  • The Request contract stores a minimum nonce per funding address.
  • An offer’s nonce must be strictly greater than the stored minimum.
  • Nonce values start at 1; nonce 0 is always invalid.
  • Nonces do not need to be sequential; gaps are allowed.

To cancel an outstanding offer without an on-chain transaction, replace or withdraw it through the off-chain auction flow before it is consumed. This is operationally convenient, but it does not invalidate an already-signed payload on-chain by itself.

To irrevocably invalidate all offers at or below a given nonce, call setMinNonce(newMinNonce) on the Request contract. This updates the stored minimum nonce for your funding address.

After calling setMinNonce(5):

  • all offers with nonce 1, 2, 3, 4, or 5 are permanently invalid
  • future offers must use nonce 6 or higher

Use hard cancellation when you need certainty that an old signature cannot be replayed, regardless of off-chain coordination state.

Scenario Recommended approach
Replace an offer with better terms Soft update through the auction flow
Withdraw from an auction entirely Hard cancel with setMinNonce()
Routine offer refreshes Reissue offers with a deliberate nonce policy
Emergency: revoke all outstanding offers Hard cancel with a high nonce value

You can validate most BF integration logic on Sepolia or another test environment without recreating the full off-chain matching engine.

  1. Create or deploy a Request with a mock puller and mock owner you control.
  2. If you are testing the push-funded path, call authorizeMinting() from the mock owner or consumer role.
  3. From the BF funding address, approve the asset and call mint().
  4. From the mock puller, call pullFunds() to move capital into the simulated strategy.
  5. From the mock puller, call repay() with principal plus return.
  6. From the mock owner, call setRepaid().
  7. Redeem PT/YT from the BF funding address.

This is enough to verify approvals, PT/YT issuance, repayment gating, and redemption sequencing without running the full auction service.

The current system sources bridge loan capital exclusively from off-chain bridge facilitators who submit offers through the auction engine. Future versions of Grunt may also support on-chain lending protocols as capital sources, including:

  • CAP — programmatic lending vaults
  • Wildcat — permissioned credit markets
  • 3Jane — structured credit

These integrations would allow the Request contract to draw capital directly from on-chain lending pools alongside or instead of bridge facilitator offers. The PT/YT settlement mechanics remain the same; the difference is in how capital enters the Request.