算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
jb-omnichain-payout-limits【设计与创意】Omnichain projects have per-chain payout limits, not aggregate limits. This is a fundamental constraint with no perfect solution. Use when: (1) user wants a fixed total fundraising cap across chains, (2) asking about aggregate payout limits on omnichain projects, (3) designing omnichain projects with payout constraints, (4) exploring oracle or monito…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Note: This applies to projects using payout limits (standard Juicebox projects with treasury distributions). Revnets don't use payout limits - they use unlimited surplus allowances for loans via REVLoans, so this limitation doesn't affect them.
Payout limits in Juicebox V5 are per-chain, not aggregate.
When you deploy an omnichain project (linked via Suckers), each chain has its own independent payout limit. Setting a 10 ETH payout limit on a 5-chain project means you could potentially pay out 50 ETH total (10 ETH × 5 chains), not 10 ETH total.
┌─────────────────────────────────────────────────────────────────────┐
│ Omnichain Project with 10 ETH "Payout Limit" │
│ │
│ Chain 1 (Ethereum): Limit = 10 ETH → Can pay out 10 ETH │
│ Chain 2 (Optimism): Limit = 10 ETH → Can pay out 10 ETH │
│ Chain 3 (Base): Limit = 10 ETH → Can pay out 10 ETH │
│ Chain 4 (Arbitrum): Limit = 10 ETH → Can pay out 10 ETH │
│ ───────────────── │
│ TOTAL: 40 ETH possible │
│ │
│ User expectation: 10 ETH max │
│ Reality: 40 ETH max │
└─────────────────────────────────────────────────────────────────────┘
Cross-chain state is asynchronous. There is no atomic way to know the aggregate raised/paid across all chains in real-time.
Any solution that coordinates cross-chain state introduces:
This is not a bug in Juicebox - it's a fundamental property of multi-chain systems.
Best for: Projects where approximate limits are acceptable
Set per-chain limits that sum to your target, accepting that distribution may not match expectations.
// Goal: ~100 ETH total across 5 chains
// Strategy: 20 ETH limit per chain
// Each chain's ruleset config:
fundAccessLimitGroups: [{
terminal: MULTI_TERMINAL,
token: NATIVE_TOKEN,
payoutLimits: [{
amount: 20 ether, // Per-chain limit
currency: 1 // ETH
}],
surplusAllowances: []
}]
Tradeoffs:
When to use:
Best for: Projects with active operators who can respond quickly
Use Bendystraw to monitor aggregate raises, manually pause payments when approaching threshold.
const BENDYSTRAW_API = 'https://bendystraw.xyz/{API_KEY}/graphql';
// Query aggregate balance across all chains
async function getAggregateBalance(suckerGroupId: string) {
const query = `
query($id: String!) {
suckerGroup(id: $id) {
balance
volume
projects_rel {
chainId
balance
}
}
}
`;
const data = await fetch(BENDYSTRAW_API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables: { id: suckerGroupId } })
}).then(r => r.json());
return data.data.suckerGroup;
}
// Monitoring loop
async function monitorAndAlert(suckerGroupId: string, threshold: bigint) {
const group = await getAggregateBalance(suckerGroupId);
const balance = BigInt(group.balance);
if (balance >= threshold * 90n / 100n) {
// 90% of threshold - send alert
console.log('ALERT: Approaching threshold, consider pausing');
}
if (balance >= threshold) {
// At threshold - operator should pause
console.log('THRESHOLD REACHED: Pause payments now');
// Could trigger notification (email, Telegram, Discord webhook)
}
}
Manual pause action:
// Project owner calls on each chain
await controller.queueRulesetsOf(projectId, [{
// ... existing config
metadata: {
// ... existing metadata
pausePay: true // Disable new payments
}
}]);
Tradeoffs:
When to use:
Best for: Projects wanting automation without full oracle complexity
A cron job monitors aggregate balance via Bendystraw, uses Relayr to pause payments across all chains when threshold approaches.
┌─────────────────────────────────────────────────────────────────────┐
│ Automated Threshold Monitor │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Cron Job │────►│ Bendystraw │────►│ Check │ │
│ │ (every 5m) │ │ Query │ │ Threshold │ │
│ └──────────────┘ └──────────────┘ └──────┬───────┘ │
│ │ │
│ threshold met? │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Relayr Bundle: Pause payments on all chains │ │
│ │ │ │
│ │ Chain 1: queueRulesetsOf(projectId, {pausePay: true}) │ │
│ │ Chain 2: queueRulesetsOf(projectId, {pausePay: true}) │ │
│ │ Chain 3: queueRulesetsOf(projectId, {pausePay: true}) │ │
│ │ Chain 4: queueRulesetsOf(projectId, {pausePay: true}) │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Implementation requirements:
QUEUE_RULESETS permission on each chain's project// Simplified cron handler
async function checkAndPauseIfNeeded(config: {
suckerGroupId: string;
threshold: bigint;
projectIds: { chainId: number; projectId: number }[];
operatorKey: string;
}) {
// 1. Check aggregate balance
const group = await getAggregateBalance(config.suckerGroupId);
const balance = BigInt(group.balance);
if (balance < config.threshold) {
return; // Under threshold, do nothing
}
// 2. Build pause transactions for each chain
const pauseTxs = config.projectIds.map(({ chainId, projectId }) => ({
chain: chainId,
target: CONTROLLER_ADDRESSES[chainId],
data: encodeFunctionData({
abi: JB_CONTROLLER_ABI,
functionName: 'queueRulesetsOf',
args: [projectId, /* rulesetConfigs with pausePay: true */]
}),
value: '0'
}));
// 3. Submit via Relayr
const bundle = await relayr.createBundle(pauseTxs);
await relayr.payForBundle(bundle, config.operatorKey);
console.log(`Paused payments across ${config.projectIds.length} chains`);
}
Tradeoffs:
When to use:
Best for: Projects requiring hard limits with strong guarantees
A pay hook checks an oracle for aggregate state before allowing payments. The oracle is updated by a relayer watching all chains.
┌─────────────────────────────────────────────────────────────────────┐
│ Oracle-Based Aggregate Limit │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Oracle Contract (deployed on each chain) │ │
│ │ ├── aggregateBalance: uint256 (updated by relayer) │ │
│ │ ├── threshold: uint256 │ │
│ │ └── lastUpdate: uint256 (timestamp) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ update │
│ ┌────────────────────────┴────────────────────────────────────┐ │
│ │ Relayer Service │ │
│ │ ├── Watches PayEvents on all chains │ │
│ │ ├── Calculates aggregate balance │ │
│ │ └── Updates oracle on all chains │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Pay Flow: │
│ User → pay() → PayHook.beforePayRecordedWith() → Oracle.check() │
│ │
│ If oracle.aggregateBalance + payAmount > threshold: │
│ REVERT "Aggregate limit exceeded" │
└─────────────────────────────────────────────────────────────────────┘
Implementation sketch:
// Oracle contract
contract AggregateBalanceOracle {
uint256 public aggregateBalance;
uint256 public threshold;
uint256 public lastUpdate;
address public relayer;
function updateBalance(uint256 newBalance) external {
require(msg.sender == relayer, "UNAUTHORIZED");
aggregateBalance = newBalance;
lastUpdate = block.timestamp;
}
function wouldExceedThreshold(uint256 additionalAmount) external view returns (bool) {
return aggregateBalance + additionalAmount > threshold;
}
}
// Pay hook that checks oracle
contract AggregateLimitPayHook is IJBPayHook {
AggregateBalanceOracle public oracle;
uint256 public maxStaleness = 5 minutes;
function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
external view override returns (uint256, uint256, uint256, JBPayHookSpecification[] memory)
{
// Check oracle freshness
require(
block.timestamp - oracle.lastUpdate() <= maxStaleness,
"Oracle stale"
);
// Check aggregate limit
require(
!oracle.wouldExceedThreshold(context.amount.value),
"Aggregate limit exceeded"
);
// Allow payment
return (context.weight, context.newlyIssuedTokenCount, context.totalSupply, new JBPayHookSpecification[](0));
}
}
Tradeoffs:
When to use:
Current status: No off-the-shelf implementation exists. Would need custom development.
Best for: Projects that can centralize treasury operations
Accept payments on all chains, but bridge all funds to a single "home" chain for treasury operations. Payout limit on home chain is the only one that matters.
┌─────────────────────────────────────────────────────────────────────┐
│ Single-Chain Treasury Pattern │
│ │
│ Chain 1 (Home): Treasury lives here │
│ ├── Payout limit: 100 ETH (THIS IS THE REAL LIMIT) │
│ ├── Accepts payments │
│ └── All payouts happen here │
│ │
│ Chain 2-4: Payment collection only │
│ ├── Payout limit: 0 (no direct payouts) │
│ ├── Accept payments │
│ └── Funds bridged to Chain 1 via Suckers │
│ │
│ Flow: │
│ User pays on Chain 2 → Tokens issued on Chain 2 │
│ → User bridges funds to Chain 1 (manual) │
│ → Chain 1 treasury grows │
│ → Payouts from Chain 1 only │
└─────────────────────────────────────────────────────────────────────┘
The blocker: Suckers require explicit user action to bridge. The project cannot automatically sweep funds from other chains to the home chain. Users must:
prepare() on the source chainclaim() on the destination chainPotential workarounds:
Tradeoffs:
When to use:
| Approach | Enforcement | Latency | Trust Required | Complexity | Best For |
|---|---|---|---|---|---|
| Accept & Design | Soft | N/A | None | Low | Soft caps |
| Manual Monitoring | Soft | Minutes | Operator | Low | Active teams |
| Cron + Relayr | Soft | ~5-10 min | Operator | Medium | Automation |
| Oracle Pay Hook | Hard* | Seconds | Relayer | High | Compliance |
| Single-Chain | Medium | N/A | None | Medium | Centralized ops |
*Oracle enforcement has latency window during updates
Answer: This is very difficult in omnichain. Consider:
Answer: Use Approach 1 (Accept & Design) or Approach 2 (Manual Monitoring)
Answer: Use Approach 3 (Cron + Relayr)
Answer: This limitation doesn't apply to revnets
Theoretically, but it would require:
This is a significant protocol-level change, not a simple feature add.
Yes - a community-run oracle service could:
This could be built as infrastructure, but requires:
/jb-omnichain-ui - Building omnichain frontends/jb-suckers - Cross-chain token bridging mechanics/jb-bendystraw - Querying aggregate project data/jb-relayr - Multi-chain transaction bundling/jb-fund-access-limits - Per-chain payout limit configuration