sqrtDAO docs

Epoch-based distribution

Every distribution on sqrtDAO runs on the same engine: a fixed supply of a distribution token is released over a series of equal-length epochs, and anyone who locks the participation token during an epoch earns a pro-rata slice of that epoch's reward. This page walks through the lifecycle.

One sale, many epochs

A distribution starts at a fixed timestamp and is divided into NUMBER_OF_EPOCHS windows of EPOCH_DURATION seconds each. The current epoch is derived purely from time — no oracle, no keeper:

DistributorV1.sol — currentEpoch()
function currentEpoch() public view returns (uint256) {
    return (block.timestamp - STARTING_TIMESTAMP) / EPOCH_DURATION;
}

Participate

To join, you call participate with an amount per epoch and a range of epochs. The full cost (amountPerEpoch × range.length) is pulled upfront, and your address accumulates weight in every epoch of the range. You can also participate on behalf of a different recipient:

DistributorV1.sol — participate() (trimmed)
PARTICIPATION_TOKEN.safeTransferFrom(msg.sender, address(this), _range.length * _amountPerEpoch);

for (uint256 i = 0; i < _range.length; i++) {
    uint256 epoch = _range.from + i;
    epochTotalParticipation[epoch] += _amountPerEpoch;
    epochUserParticipation[epoch][_recipient] += _amountPerEpoch;
}
  • Each epoch enforces a MIN_PARTICIPATION floor per participant.
  • A distribution can start allowlisted: while the allowlist window is active, only signatures from a trusted signer can participate; after it expires the doors open for everyone.
  • Whether future epochs accept participation is a per-distribution flag (ALLOW_FUTURE_EPOCH_PARTICIPATION).

Rewards per epoch

How much of the distribution token each epoch releases is decided by a pluggable emission function. Three presets ship with v1:

  • FixedEmission — same reward every epoch.
  • LinearEmission — reward = base + slope × epoch, so it can ramp up or wind down linearly.
  • ExponentialEmission— reward = initial × factor^epoch, supporting both growth (>1) and decay (<1).
DistributorV1.sol — rewardOf()
function rewardOf(uint256 epoch) public view returns (uint256) {
    return emissionFunction.emissionContract.calculate(emissionFunction.curveConfig, epoch);
}

Claiming

Once an epoch has ended and its CLAIM_DELAY_SECONDS have passed, your slice is:

DistributorV1.sol — claim() (core math)
claimAmount += (epochUserParticipation[epoch][_user] * rewardOf(epoch))
    / epochTotalParticipation[epoch];

Your share is simply your weight divided by the epoch's total participation. Claims are batchable across ranges, and third parties can claim on someone else's behalf — the account owner sets an optional fee in basis points (setClaimFeeBps) to pay whoever runs the claiming bot for them.

Draining epoch funds

The participation token locked in an epoch doesn't sit there forever. After an epoch ends, anyone can call callDrainHook, which forwards the whole epoch fund to the distribution's configured shares — percentage cuts in basis points that must sum to 100%:

DistributorV1.sol — callDrainHook() (trimmed)
uint256 fund;
for (uint256 i = nextEpochToRelease; i < currEpoch; i++) {
    fund += epochTotalParticipation[i];
}
nextEpochToRelease = currEpoch;

for (uint256 i = 0; i < shares.length; i++) {
    shares[i].approveAndCall(PARTICIPATION_TOKEN, fund);
}
Shares make distributions composable: one share might send a cut to the protocol treasury, another might buy back and burn the distributed token — see buy back & burn.