Tip Streams

Tip streams are a mechanism designed to distribute fees among specific entities within a vault. These entities share a portion of the fees based on predefined rules. This process involves minting additional shares as tips, which slightly dilutes the overall value of existing shares. To keep the system fair and sustainable, the yield generated by the vault must outpace the fees distributed. For context, we charge a fee of 1% of the Assets Under Management (AUM), which forms the basis of the tips distributed.

How Tip Streams Work

In any vault, the fee is set at the vault level rather than individually for entities. Tip streams define how this fee is divided among those entitled to a share of the tips. A key part of the process is setting the tip rate, which determines the proportion of the fee that will be distributed as tips. These tips are collected in a central pool known as the TipJar.

When tips are distributed, they are not taken directly from the vault’s assets. Instead, additional shares are minted to represent the tips. These newly minted shares are added to the TipJar. This increases the total number of shares in the vault, slightly reducing the value of each share since the same amount of assets is now split among a larger number of shares.

The 1% AUM fee ensures a steady and predictable source for tip distribution. This percentage is applied to the total assets managed by the vault, making the system straightforward and easy to calculate.

Note: The Ethereum Mainnet $ETH vault charges a 0.3% AUM fees, instead of the 1% for stablecoin vaults.

TipStream Management Functions

addTipStream(TipStream memory tipStream)

function addTipStream(TipStream memory tipStream) external onlyGovernor returns (uint256 lockedUntilEpoch) {
    if (tipStream.recipient == address(0)) {
        revert InvalidTipStreamRecipient();
    }
    if (tipStreams[tipStream.recipient].recipient != address(0)) {
        revert TipStreamAlreadyExists(tipStream.recipient);
    }
    if (tipStream.lockedUntilEpoch > block.timestamp + MAX_ALLOWED_LOCKED_UNTIL_EPOCH) {
        revert TipStreamLockedForTooLong(tipStream.recipient);
    }
    _validateTipStreamAllocation(tipStream.allocation, toPercentage(0));

    tipStreams[tipStream.recipient] = tipStream;
    tipStreamRecipients.push(tipStream.recipient);

    emit TipStreamAdded(tipStream);

    return tipStream.lockedUntilEpoch;
}

Distribution Functions

shake(address fleetCommander)

function _shake(address fleetCommander_) internal {
    if (!IHarborCommand(harborCommand()).activeFleetCommanders(fleetCommander_)) {
        revert InvalidFleetCommanderAddress();
    }

    IFleetCommander fleetCommander = IFleetCommander(fleetCommander_);
    uint256 shares = fleetCommander.balanceOf(address(this));
    
    if (shares == 0) return;

    uint256 withdrawnAssets = fleetCommander.redeem(
        Constants.MAX_UINT256,
        address(this),
        address(this)
    );

    if (withdrawnAssets == 0) return;

    IERC20 underlyingAsset = IERC20(fleetCommander.asset());
    uint256 totalDistributed = 0;
    Percentage totalAllocated = toPercentage(0);

    for (uint256 i = 0; i < tipStreamRecipients.length; i++) {
        address recipient = tipStreamRecipients[i];
        Percentage allocation = tipStreams[recipient].allocation;
        totalAllocated = totalAllocated + allocation;

        uint256 amount = (totalAllocated == PERCENTAGE_100) ? 
            withdrawnAssets - totalDistributed :
            withdrawnAssets.applyPercentage(allocation);

        if (amount > 0) {
            underlyingAsset.safeTransfer(recipient, amount);
            totalDistributed += amount;
        }
    }

    uint256 remaining = withdrawnAssets - totalDistributed;
    if (remaining > 0) {
        underlyingAsset.safeTransfer(treasury(), remaining);
    }
}

Query Functions

getTotalAllocation()

function getTotalAllocation() public view returns (Percentage total) {
    total = toPercentage(0);
    for (uint256 i = 0; i < tipStreamRecipients.length; i++) {
        total = total + tipStreams[tipStreamRecipients[i]].allocation;
    }
}

The contract uses the OpenZeppelin SafeERC20 library for safe token transfers and includes comprehensive error handling for invalid operations.

Last updated

Was this helpful?