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/zapOutcall 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)
- Requires
factory.isBasket(basket)(elseNotABasket()). - Pulls
usdgInUSDG from the caller. _executeLegs(legs, usdg)— runs each allowlisted swap leg, spending USDG to acquire constituents.- Transfers the router's full balance of every
basket.allTokens()into the basket. netShares = basket.mintFromDeposit(msg.sender, minSharesOut)— the basket enforcesminSharesOuton-chain (the real slippage floor).- 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)
- Requires
factory.isBasket(basket). - Pulls
sharesInbasket shares from the caller. basket.redeemInKind(sharesIn, address(this))— releases the constituents in-kind (exit fee applied inside the basket)._executeLegsMulti(legs, tokens)— runs each allowlisted swap leg to produce USDG.usdgOut = usdg.balanceOf(this); revertsSwapFailed()ifusdgOut < minUsdgOut; transfers USDG to the caller.- 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, checksallowedVenue,forceApproves the venue for the router's full USDG balance, executesvenue.call(leg.data)(revertsSwapFailed()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.