Absorb (technical)
Absorb is a single-stock Greenback launch with an extra immutable 1% trade fee that funds a backing-stock pot, and a sell-only shield that can fill a capped slice of a sell at the current spot so that slice never hits the bonding curve or the Uniswap pool.
Backing is still one listed stock. Graduation is still $6,000 of curve backing. The launched token is a normal ERC-20. What changes is the pot, the extra fee, and how sells are split.
Absorb fee
Depth cap
Shield
Pot asset
Spot
Empty pot
Name in the contracts
Jump: constants · create · fees · sell split · donate · graduation · V4 hook · app
Contracts and roles
Absorb does not reuse the single-stock factory or StockBackHook. It has its own factory, pair deployer, curve, V4 hook, and graduator so the extra 1% and the shield cannot be mixed with a normal launch.
0xE1261C…82CaFB0xE1261C9efA8A13531eeC547a57d544c86582CaFB
launch() + allowlist + V4 config
0xd79bD8…4df6280xd79bD871Dc6E4ad01F25937b887BC637C94df628
Deploys token + AbsorbBondingCurve
/// @notice Single-stock launches with a sell-only absorb shield
/// (pre- and post-grad).
/// @dev Own V4 hook + graduator. Do not reuse StockBackFactory's hook.AbsorbLaunchDeployer exists so the factory stays under the 24KB contract size limit. It mints the 1B token, deploys AbsorbBondingCurve, points the token at the curve, and transfers the full supply to the curve.
StockBackToken token = new StockBackToken(
name, symbol, imageURI, description, metadataURI,
CurveConstants.TOTAL_SUPPLY, address(this), address(this)
);
AbsorbBondingCurve curve = new AbsorbBondingCurve(
tokenAddr, stockToken, oracle, creator,
leverage, graduationThresholdUsd8, factory, holderTaxBps
);
token.setCurve(curveAddr, holderTaxBps > 0);
IERC20(tokenAddr).safeTransfer(curveAddr, CurveConstants.TOTAL_SUPPLY);
token.transferOwnership(curveAddr);Immutable constants
Both the curve and the hook hardcode the same two numbers. They cannot be changed per launch or by the factory owner.
uint256 public constant ABSORB_FEE_BPS = 100; // 1%
uint256 public constant MAX_ABSORB_DEPTH_BPS = 2_000; // 20%
uint256 public constant BPS = 10_000; // inheriteduint256 public constant ABSORB_FEE_BPS = 100;
uint256 public constant MAX_ABSORB_DEPTH_BPS = 2_000;
address public constant DEAD = 0x000000000000000000000000000000000000dEaD;On-chain state you can read:
absorbPotRaw(curve) orpot(poolId)(hook): backing stock reserved for the shield, in raw stock units.shieldedMain(curve only): launched tokens taken off the AMM by the shield, held until graduation so they can seed V4 instead of going to the burn address.totalAbsorbFees: lifetime 1% skim into the pot (curve).
Create and optional pot seed
On Create, choose Absorb. The wallet approves the Absorb factory as spender, not the single-stock factory. initialAbsorbStock is optional raw stock donated into the pot in the same transaction.
struct LaunchParams {
string name;
string symbol;
string imageURI;
string description;
string metadataURI;
address stockToken;
address oracle;
uint256 leverageMultiplier; // factory forces 1e18
uint256 initialBuyEthWei; // optional ETH seed buy
uint256 minStockOut;
address referrer;
uint16 holderTaxBps; // 0..1000, immutable
uint256 initialAbsorbStock; // optional pot seed, raw stock
}if (p.initialAbsorbStock > 0) {
IERC20(p.stockToken).safeTransferFrom(
msg.sender, address(this), p.initialAbsorbStock
);
IERC20(p.stockToken).forceApprove(curveAddr, p.initialAbsorbStock);
AbsorbBondingCurve(curveAddr).donate(p.initialAbsorbStock);
emit AbsorbSeeded(curveAddr, p.initialAbsorbStock);
}await execute({
token: backing.address,
spender: ABSORB_FACTORY,
amount: absorbSeed, // 0 skips approve
skipApprove: absorbSeed === 0n,
call: {
address: ABSORB_FACTORY,
functionName: "launch",
args: [{
name, symbol: ticker, imageURI, description, metadataURI,
stockToken: backing.address,
oracle: zeroAddress, // factory uses oracleOfStock
leverageMultiplier: 10n ** 18n,
initialBuyEthWei: seed,
minStockOut: 0n,
referrer: zeroAddress,
holderTaxBps,
initialAbsorbStock: absorbSeed,
}],
value: launchFee + seed,
},
});Checks at launch
msg.value must cover launch fee plus any ETH seed buy. Excess ETH is refunded.Fee stack (every trade)
Fees are sequential and paid in backing stock. The Absorb 1% is taken last, from whatever is left after protocol, creator, and holder tax.
For a stock notional N (raw units after the trade is sized):
- Protocol + creator:
pc = N × (protocolFeeBps + creatorFeeBps) / 10_000. Defaults 50 + 50 = 100 bps. Paid out immediately. - Holder tax:
tax = (N − pc) × holderTaxBps / 10_000. Zero if the launch set tax off. Accrues to holders. - Absorb:
absorb = (N − pc − tax) × 100 / 10_000. Added to the pot. Never burned. Never sent to LP.
Seller or buyer receives N − pc − tax − absorb. On a buy, that net stock is what actually enters realStockReserve.
// BondingCurve._buyWithStock
uint256 protocolCreatorFee = _takeFees(stockInRaw);
uint256 afterPC = stockInRaw - protocolCreatorFee;
uint256 tax = _takeHolderTax(afterPC);
uint256 afterTax = afterPC - tax;
uint256 absorbFee = _takeAbsorbFee(afterTax); // 0 on normal curve
uint256 stockNet = afterTax - absorbFee;
realStockReserve += _to18(stockNet);
// AbsorbBondingCurve._takeAbsorbFee
fee = (afterTax * ABSORB_FEE_BPS) / BPS; // 1%
absorbPotRaw += fee;
totalAbsorbFees += fee;
emit AbsorbFeeTaken(fee);Buys are not shielded
Worked numbers. Buy 100 stock, 0% holder tax:
- Protocol + creator take 1.00
- Absorb pot takes 0.99 (1% of 99)
- Curve reserve gets 98.01
Same buy with 5% holder tax: after 1.00 protocol/creator, tax takes 4.95, Absorb takes 0.9405, curve gets 93.1095.
Sell split on the curve
A sell is two legs: a constant-price shield at the current spot, then the unshielded remainder on the normal curve. Fees are taken on the combined stock out, not per leg.
Spot on the curve (same as a normal launch):
virtualStock = VIRTUAL_STOCK_RESERVE + realStockReserve
virtualToken = VIRTUAL_TOKEN_RESERVE + (TOKENS_FOR_SALE - tokensSold)
// launched tokens coverable at spot for stock S:
// tokens = S * virtualToken / virtualStockPer-sell cap before the split:
function maxShieldRaw() public view returns (uint256) {
uint256 depthCap = _from18(
(realStockReserve * MAX_ABSORB_DEPTH_BPS) / BPS
); // 20% of curve stock depth
uint256 pot = absorbPotRaw;
return pot < depthCap ? pot : depthCap;
}If the cap is 0 (empty pot or empty reserve), the whole sell is a normal curve sell. Otherwise:
uint256 capRaw = maxShieldRaw();
uint256 vs = VIRTUAL_STOCK_RESERVE + realStockReserve;
uint256 vt = VIRTUAL_TOKEN_RESERVE + (TOKENS_FOR_SALE - tokensSold);
// tokens the pot can buy at current spot (constant price, no curve move)
shieldTokens = (capNorm * vt) / vs;
if (shieldTokens > tokensIn) {
shieldTokens = tokensIn;
capNorm = (shieldTokens * vs) / vt; // only pay for what you take
capRaw = _from18(capNorm);
}
remainder = tokensIn - shieldTokens;
curveStockNorm = remainder == 0 ? 0 : _stockOutForTokensIn(remainder);Then the sell applies both legs:
(shieldTokens, shieldRaw, remainder, curveStockNorm) = _splitShield(tokensIn);
if (remainder > 0) {
realStockReserve -= curveStockNorm;
tokensSold -= remainder; // remainder went back on the curve
}
if (shieldTokens > 0) {
shieldedMain += shieldTokens; // pulled off the AMM, held for V4
absorbPotRaw -= shieldRaw;
emit AbsorbShielded(shieldTokens, shieldRaw, absorbPotRaw);
}
grossRaw = shieldRaw + _from18(curveStockNorm);
// then the same fee stack as a buy, on grossRaw
protocolCreatorFee = _takeFees(grossRaw);
tax = _takeHolderTax(afterPC);
absorbFee = _takeAbsorbFee(afterTax); // 1% back into the pot
stockOutRaw = afterTax - absorbFee;Only-up pressure on the curve
realStockReserve. They sit in shieldedMain. The curve only moves on the remainder. That is why a fully shielded sell does not walk the price down the bonding curve.quoteSell is overridden so the UI and routers see the same split (gross from shield + curve, then fees).
function quoteSell(uint256 tokensIn)
external view override
returns (uint256 stockOutRaw, uint256 priceUsd8After)
{
(uint256 grossRaw, uint256 curveStockNorm, uint256 remainder) =
_quoteGross(tokensIn);
uint256 protocolCreator = protocolFeeBps() + creatorFeeBps();
uint256 feePC = (grossRaw * protocolCreator) / BPS;
uint256 afterPC = grossRaw - feePC;
uint256 tax = (afterPC * holderTaxBps) / BPS;
uint256 afterTax = afterPC - tax;
uint256 absorbFee = _quoteAbsorbFee(afterTax);
stockOutRaw = afterTax - absorbFee;
// price after uses remainder back on the virtual token reserve
}Worked sell. Pot = 10 stock. Curve depth = 80 stock. Cap = min(10, 16) = 10. Trader sells a size the pot can fully cover at spot:
- Shield pays 10 stock at current spot, tokens move to shielded inventory
- Curve reserve and tokensSold do not change for that slice
- Fees (1% + tax + 1% Absorb) come out of the 10 stock paid
- The 1% Absorb fee goes back into the pot in the same transaction
If the sell is larger than the cap, only the cap is shielded. The rest is a normal _stockOutForTokensIn curve sell and does move price.
Donate (permissionless)
Anyone can add backing stock to the pot. Before graduation, donate on the curve. After graduation, donate on the hook with the pool id. Amount 0 reverts on the curve and is a no-op on the hook.
function donate(uint256 amountRaw) external nonReentrant {
if (graduated) revert GraduatedAlready();
if (amountRaw == 0) revert ZeroAmount();
stockToken.safeTransferFrom(msg.sender, address(this), amountRaw);
absorbPotRaw += amountRaw;
emit AbsorbDonated(msg.sender, amountRaw);
}function donate(bytes32 poolId, uint256 amount) external {
PoolId id = PoolId.wrap(poolId);
address stock = stockOf[id];
if (stock == address(0)) revert UnknownPool();
if (amount == 0) return;
IERC20(stock).safeTransferFrom(msg.sender, address(this), amount);
pot[id] += amount;
emit AbsorbDonated(id, msg.sender, amount);
}The token page pot panel reads absorbPotRaw or pot(poolId) and calls the matching donate. Approve the curve before grad, the hook after.
Graduation
Trigger is unchanged: getBackingValueUsd() >= graduationThresholdUsd8 ($6,000). The pot is not part of backing and does not count toward the threshold. The pot does not go into the Uniswap LP.
Two Absorb-specific hooks run inside the shared graduate path:
- Extra launched tokens:
shieldedMainis added to the V4 seed instead of being sent to the burn address. Those tokens were already pulled off the curve by the shield. - After the pool exists, remaining pot stock is transferred to AbsorbHook and credited to that pool id.
tokensForPool = /* ideal curve spot match, capped by balance */;
tokensForPool += _extraGraduationTokens(); // Absorb: shieldedMain
if (tokensForPool > onCurve) tokensForPool = onCurve;
tokensDead = onCurve - tokensForPool;
if (tokensDead > 0) token.safeTransfer(DEAD, tokensDead);
// seed V4 with tokensForPool + curve stock reserve
_afterGraduate(graduationPoolId);function _extraGraduationTokens() internal view override returns (uint256) {
return shieldedMain;
}
function _afterGraduate(bytes32 poolId) internal override {
uint256 pot = absorbPotRaw;
if (pot == 0) return;
address hook = IV4Config(factory).v4Hook();
absorbPotRaw = 0;
stockToken.safeTransfer(hook, pot);
IAbsorbHook(hook).creditPot(poolId, pot);
}function creditPot(bytes32 poolId, uint256 amount) external {
PoolId id = PoolId.wrap(poolId);
if (msg.sender != curveOf[id]) revert OnlyCurve();
if (amount == 0) return;
pot[id] += amount;
emit PotCredited(id, amount);
}After graduation, curve buy/sell revert with GraduatedAlready. Trading is the Uniswap V4 pool. Shared pool details: Graduation.
Post-grad AbsorbHook
The hook uses the same CREATE2 permission flags as StockBackHook so the graduator can register the pool. It adds a sell-only shield in beforeSwap and the same fee stack on every swap.
beforeInitialize, beforeSwap, beforeSwapReturnDelta,
afterSwap, afterSwapReturnDeltaA sell is launched token in, stock out (zeroForOne points away from the stock). Buys (stock in) are never absorbed.
function _isMainSell(key, params, stock) internal pure returns (bool) {
bool stockIs0 = Currency.unwrap(key.currency0) == stock;
return stockIs0 != params.zeroForOne; // selling the other asset
}Exact-in sells only. The hook takes a slice of the specified input, pays stock from the pot, and burns the taken launched tokens via PoolManager.take to the dead address. The pool already exists, so those tokens are not saved for a later seed.
if (mainSell && !stockSpecified) {
(uint256 takeMain, uint256 stockPaid) =
_absorbSlice(id, key, params, stock);
if (takeMain > 0 && stockPaid > 0) {
specifiedDelta = takeMain.toInt128(); // reduce token-in
uint256 shieldFees = _skimOwnStockFees(id, stock, stockPaid);
uint256 netStock = stockPaid - shieldFees;
unspecifiedDelta = -netStock.toInt128(); // pay stock out
poolManager.take(mainCurrency, DEAD, takeMain); // burn
// settle net stock onto PoolManager
pot[id] -= stockPaid;
emit AbsorbShielded(id, takeMain, stockPaid, pot[id]);
}
}Leave 1 wei for the pool
amountToSwap == 0. The hook never absorbs the entire exact-in amount. It always leaves at least 1 wei for the real pool swap. An empty pot never reverts the swap.if (available == 0 || params.amountSpecified >= 0) return (0, 0);
uint256 amountIn = uint256(-params.amountSpecified);
if (amountIn <= 1) return (0, 0);
uint256 depth = _poolStockDepth(key, stock);
uint256 cap = (depth * MAX_ABSORB_DEPTH_BPS) / BPS; // 20% of pool stock
if (available < cap) cap = available;
if (bal < cap) cap = hookStockBalance; // cannot overpay
(uint160 sqrtPriceX96,,,) = poolManager.getSlot0(id);
takeMain = min(tokensFor(cap, spot), amountIn);
if (takeMain >= amountIn) takeMain = amountIn - 1; // keep 1 wei swap
stockPaid = stockFor(takeMain, spot);
// clamp again if rounding walks past cap or amountInPool depth is the stock-side inventory implied by current liquidity and the full-range ticks (same spacing the graduator used):
uint128 liq = poolManager.getLiquidity(id);
sqrtLower = TickMath.getSqrtPriceAtTick(minUsableTick(spacing));
sqrtUpper = TickMath.getSqrtPriceAtTick(maxUsableTick(spacing));
amount0 = SqrtPriceMath.getAmount0Delta(sqrtP, sqrtUpper, liq, false);
amount1 = SqrtPriceMath.getAmount1Delta(sqrtLower, sqrtP, liq, false);
return stockIs0 ? amount0 : amount1;Spot conversion uses sqrtPriceX96 (constant price, no impact on the shield leg):
// token1 = token0 * (sqrtP / 2^96)^2
_token1FromToken0 = mulDiv(mulDiv(amount0, p, 2^96), p, 2^96)
_token0FromToken1 = mulDiv(mulDiv(amount1, 2^96, p), 2^96, p)Fees on the remaining pool swap use _payStockFees (take from the pool via PoolManager). Fees on the shield leg use _skimOwnStockFees (transfer from the hook’s own stock balance). Same order: protocol + creator, holder tax (credited back to the curve), then 1% into pot[id].
pcFee = notional * (protocolBps + creatorBps) / 10_000
tax = (notional - pcFee) * holderTaxBps / 10_000
absorbFee = (notional - pcFee - tax) * 100 / 10_000
// absorbFee stays on the hook and increments pot[id]What the app reads and writes
Explore and the token page tag Absorb markets with a violet badge (marketKind === "absorb"). The pot panel is the only Absorb-specific trade UI.
// pre-grad
absorbPotRaw() // pot in raw stock
shieldedMain() // launched tokens held for V4 seed
// post-grad
pot(poolId) // pot now on AbsorbHook// pre-grad
approve(stock → curve)
curve.donate(amountRaw)
// post-grad
approve(stock → hook)
hook.donate(poolId, amountRaw)Trade UI is the same buy/sell as a normal launch. Quotes go through the overridden quoteSell so the shield is in the number you see before you sign.
Events
event AbsorbDonated(address indexed donor, uint256 amount);
event AbsorbFeeTaken(uint256 amount);
event AbsorbShielded(uint256 tokensIn, uint256 stockPaid, uint256 remainingPot);
event AbsorbSeeded(address indexed curve, uint256 stockAmount); // factoryevent AbsorbFeeTaken(PoolId indexed poolId, uint256 amount);
event AbsorbShielded(
PoolId indexed poolId,
uint256 mainBurned,
uint256 stockPaid,
uint256 remainingPot
);
event AbsorbDonated(PoolId indexed poolId, address indexed donor, uint256 amount);
event PotCredited(PoolId indexed poolId, uint256 amount);Limits and failure modes
- Total take is higher than a normal launch: 1% protocol+creator, plus holder tax if set, plus 1% Absorb. See Fees.
- A large sell can still move the curve or pool. Only
min(pot, 20% depth, this sell)is shielded. After grad, one extra wei always hits the pool. - The shield uses spot. In a fast market that can be better or worse than a TWAP. There is no time-weighted oracle on the shield.
- Empty pot: sell still works. The 1% still refills. Nothing reverts because the shield is dry.
- Pot stock never counts as curve backing and never seeds LP. Donating does not move graduation progress.
- Confirm the violet Absorb badge and the backing stock before you buy. The spender at create must be AbsorbFactory.
Broader risk notes: Risks.