HoodZapRouter

contracts/src/periphery/HoodZapRouter.sol · Solidity 0.8.26

The atomic zap periphery: one transaction = pull USDG → run allowlisted swaps → deliver constituents → mint (zap-in), or the inverse (zap-out). It holds nothing between transactions and sweeps every residual back to the caller.

  • Inherits: Ownable2Step, ReentrancyGuard.

Tokens sent directly to this router outside a zapIn/zapOut call are not escrowed and can be claimed by whoever calls next — the same policy as donations sent directly to a Basket.

Storage

IBasketFactoryLike public immutable factory;   // isBasket(address)
IWhitelistLike public immutable whitelist;     // isAllowed(address)
IERC20 public immutable usdg;
mapping(address => bool) public allowedVenue;  // venue allowlist

struct SwapLeg { address venue; bytes data; } // matches the backend's leg encoding

zapIn

function zapIn(address basket, uint256 usdgIn, SwapLeg[] calldata legs, uint256 minSharesOut)
    external nonReentrant returns (uint256 netShares)
  1. Requires factory.isBasket(basket) (else NotABasket()).
  2. Pulls usdgIn USDG from the caller.
  3. _executeLegs(legs, usdg) — runs each allowlisted swap leg, spending USDG to acquire constituents.
  4. Transfers the router's full balance of every basket.allTokens() into the basket.
  5. netShares = basket.mintFromDeposit(msg.sender, minSharesOut) — the basket enforces minSharesOut on-chain (the real slippage floor).
  6. Sweeps leftover USDG back to the caller; emits ZappedIn.

zapOut

function zapOut(address basket, uint256 sharesIn, SwapLeg[] calldata legs, uint256 minUsdgOut)
    external nonReentrant returns (uint256 usdgOut)
  1. Requires factory.isBasket(basket).
  2. Pulls sharesIn basket shares from the caller.
  3. basket.redeemInKind(sharesIn, address(this)) — releases the constituents in-kind (exit fee applied inside the basket).
  4. _executeLegsMulti(legs, tokens) — runs each allowlisted swap leg to produce USDG.
  5. usdgOut = usdg.balanceOf(this); reverts SwapFailed() if usdgOut < minUsdgOut; transfers USDG to the caller.
  6. Sweeps any un-swapped constituent dust back; emits ZappedOut.

setVenue (owner)

function setVenue(address venue, bool ok) external onlyOwner   // emits VenueSet

Manages the venue allowlist. Reverts BadVenue() if venue is address(0), the router itself, USDG, a basket (factory.isBasket), a whitelisted constituent (whitelist.isAllowed), or has no code. This guard is essential: because leg execution approves the router's full balance to a venue and then resets it, a venue must never be able to point at USDG, a basket, or a constituent — otherwise it could drain funds.

Leg execution model

  • _executeLegs (zap-in): for each leg, checks allowedVenue, forceApproves the venue for the router's full USDG balance, executes venue.call(leg.data) (reverts SwapFailed() on failure), then resets the allowance to 0.
  • _executeLegsMulti (zap-out): same, but approves the router's balance of every constituent so a leg can sell any of them.

The concrete venue is the HoodV4SwapAdapter; its calldata's recipient is the router itself, so swap output lands back on the router for the next step.

Events & errors

event ZappedIn(address indexed user, address indexed basket, uint256 usdgIn, uint256 netShares);
event ZappedOut(address indexed user, address indexed basket, uint256 sharesIn, uint256 usdgOut);
event VenueSet(address indexed venue, bool ok);

error VenueNotAllowed(); // a leg's venue isn't allowlisted
error BadVenue();        // setVenue rejected the address
error NotABasket();      // target isn't a registered basket
error SwapFailed();      // a venue call failed, or zap-out proceeds < minUsdgOut

See Zap flow end-to-end for how the backend builds the legs and tx a client submits here.