Skip to content

Commit

Permalink
feat(budgets): implement linear vesting budget (fungible only)
Browse files Browse the repository at this point in the history
  • Loading branch information
ccashwell committed Mar 15, 2024
1 parent d8e3dc5 commit 810f612
Show file tree
Hide file tree
Showing 2 changed files with 1,043 additions and 0 deletions.
258 changes: 258 additions & 0 deletions src/budgets/VestingBudget.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.24;

import {LibZip} from "lib/solady/src/utils/LibZip.sol";
import {SafeTransferLib} from "lib/solady/src/utils/SafeTransferLib.sol";
import {ReentrancyGuard} from "lib/solady/src/utils/ReentrancyGuard.sol";

import {Budget} from "src/budgets/Budget.sol";
import {Cloneable} from "src/shared/Cloneable.sol";

/// @title Vesting Budget
/// @notice A vesting-based budget implementation that allows for the distribution of assets over time
/// @dev Take note of the following when making use of this budget type:
/// - The budget is designed to manage native and ERC20 token balances only. Using rebasing tokens or other non-standard token types may result in unexpected behavior.
/// - Any assets allocated to this type of budget will follow the vesting schedule as if they were locked from the beginning, which is to say that, if the vesting has already started, some portion of the assets will be immediately available for distribution.
/// - A vesting budget can also act as a time-lock, unlocking all assets at a specified point in time. To release assets at a specific time rather than vesting them over time, set the `start` to the desired time and the `duration` to zero.
/// - This contract is {Ownable} to enable the owner to allocate to the budget, reclaim and disburse assets from the budget, and to set authorized addresses. Additionally, the owner can transfer ownership of the budget to another address. Doing so has no effect on the vesting schedule.
contract VestingBudget is Budget, ReentrancyGuard {
using LibZip for bytes;
using SafeTransferLib for address;

/// @notice The payload for initializing a VestingBudget
struct InitPayload {
address owner;
address[] authorized;
uint64 start;
uint64 duration;
uint64 cliff;
}

/// @dev The total amount of each fungible asset distributed from the budget
mapping(address => uint256) private _distributedFungible;

/// @dev The mapping of authorized addresses
mapping(address => bool) private _isAuthorized;

/// @notice The timestamp at which the vesting schedule begins
uint64 public start;

/// @notice The duration of the vesting schedule (in seconds)
uint64 public duration;

/// @notice The duration of the cliff period (in seconds)
uint64 public cliff;

/// @notice A modifier that allows only authorized addresses to call the function
modifier onlyAuthorized() {
if (!isAuthorized(msg.sender)) revert Unauthorized();
_;
}

/// @notice Construct a new VestingBudget
/// @dev Because this contract is a base implementation, it should not be initialized through the constructor. Instead, it should be cloned and initialized using the {initialize} function.
constructor() {
_disableInitializers();
}

/// @inheritdoc Cloneable
/// @param data_ The compressed init data for the budget (see {InitPayload})
function initialize(bytes calldata data_) public virtual override initializer {
InitPayload memory init_ = abi.decode(data_.cdDecompress(), (InitPayload));

start = init_.start;
duration = init_.duration;
cliff = init_.cliff;

_initializeOwner(init_.owner);
for (uint256 i = 0; i < init_.authorized.length; i++) {
_isAuthorized[init_.authorized[i]] = true;
}
}

/// @inheritdoc Budget
/// @notice Allocates assets to the budget
/// @param data_ The compressed data for the {Transfer} request
/// @return True if the allocation was successful
/// @dev The caller must have already approved the contract to transfer the asset
/// @dev If the asset transfer fails, the allocation will revert
function allocate(bytes calldata data_) external payable virtual override returns (bool) {
Transfer memory request = abi.decode(data_.cdDecompress(), (Transfer));
if (request.assetType == AssetType.ETH) {
FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload));

// Ensure the value received is equal to the `payload.amount`
if (msg.value != payload.amount) {
revert InvalidAllocation(request.asset, payload.amount);
}
} else if (request.assetType == AssetType.ERC20) {
FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload));

// Transfer `payload.amount` of the token to this contract
request.asset.safeTransferFrom(request.target, address(this), payload.amount);
if (request.asset.balanceOf(address(this)) < payload.amount) {
revert InvalidAllocation(request.asset, payload.amount);
}
} else {
// Unsupported asset type
return false;
}

return true;
}

/// @inheritdoc Budget
/// @notice Reclaims assets from the budget
/// @param data_ The compressed {Transfer} request
/// @return True if the request was successful
/// @dev Only the owner can directly reclaim assets from the budget, and this action is not subject to the vesting schedule
/// @dev If the amount is zero, the entire available balance of the asset will be transferred to the receiver
/// @dev If the asset transfer fails for any reason, the function will revert
function reclaim(bytes calldata data_) external virtual override onlyOwner returns (bool) {
Transfer memory request = abi.decode(data_.cdDecompress(), (Transfer));
if (request.assetType == AssetType.ETH || request.assetType == AssetType.ERC20) {
FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload));
_transferFungible(
request.asset, request.target, payload.amount == 0 ? available(request.asset) : payload.amount
);
} else {
return false;
}

return true;
}

/// @inheritdoc Budget
/// @notice Disburses assets from the budget to a single recipient
/// @param data_ The compressed {Transfer} request
/// @return True if the disbursement was successful
/// @dev The maximum amount that can be disbursed is the {available} amount
function disburse(bytes calldata data_) public virtual override onlyAuthorized returns (bool) {
Transfer memory request = abi.decode(data_.cdDecompress(), (Transfer));
if (request.assetType == AssetType.ERC20 || request.assetType == AssetType.ETH) {
FungiblePayload memory payload = abi.decode(request.data, (FungiblePayload));
_transferFungible(request.asset, request.target, payload.amount);
} else {
return false;
}

return true;
}

/// @inheritdoc Budget
/// @notice Disburses assets from the budget to multiple recipients
/// @param data_ The compressed array of {Transfer} requests
/// @return True if all disbursements were successful
function disburseBatch(bytes[] calldata data_) external virtual override returns (bool) {
for (uint256 i = 0; i < data_.length; i++) {
if (!disburse(data_[i])) return false;
}

return true;
}

/// @inheritdoc Budget
function setAuthorized(address[] calldata account_, bool[] calldata authorized_)
external
virtual
override
onlyOwner
{
if (account_.length != authorized_.length) revert LengthMismatch();
for (uint256 i = 0; i < account_.length; i++) {
_isAuthorized[account_[i]] = authorized_[i];
}
}

/// @inheritdoc Budget
function isAuthorized(address account_) public view virtual override returns (bool) {
return _isAuthorized[account_] || account_ == owner();
}

/// @notice Get the end time of the vesting schedule
/// @return The end time of the vesting schedule
function end() external view virtual returns (uint256) {
return start + duration;
}

/// @inheritdoc Budget
/// @notice Get the total amount of assets allocated to the budget, including any that have been distributed
/// @param asset_ The address of the asset
/// @return The total amount of assets
/// @dev This is equal to the sum of the total current balance and the total distributed amount
function total(address asset_) external view virtual override returns (uint256) {
uint256 balance = asset_ == address(0) ? address(this).balance : asset_.balanceOf(address(this));
return _distributedFungible[asset_] + balance;
}

/// @inheritdoc Budget
/// @notice Get the amount of assets available for distribution from the budget as of the current block timestamp
/// @param asset_ The address of the asset (or the zero address for native assets)
/// @return The amount of assets currently available for distribution
/// @dev This is equal to the total vested amount minus any already distributed
function available(address asset_) public view virtual override returns (uint256) {
return _vestedAllocation(asset_, uint64(block.timestamp)) - _distributedFungible[asset_];
}

/// @inheritdoc Budget
/// @notice Get the amount of assets that have been distributed from the budget
/// @param asset_ The address of the asset
/// @return The amount of assets distributed
function distributed(address asset_) external view virtual override returns (uint256) {
return _distributedFungible[asset_];
}

/// @inheritdoc Budget
/// @dev This is a no-op as there is no local balance to reconcile
function reconcile(bytes calldata) external virtual override returns (uint256) {
return 0;
}

/// @notice Transfer assets to the recipient
/// @param asset_ The address of the asset
/// @param to_ The address of the recipient
/// @param amount_ The amount of the asset to transfer
/// @dev This function is used to transfer assets from the budget to a given recipient (typically an incentive contract)
/// @dev If the destination address is the zero address, or the transfer fails for any reason, this function will revert
function _transferFungible(address asset_, address to_, uint256 amount_) internal virtual nonReentrant {
// Increment the total amount of the asset distributed from the budget
if (to_ == address(0)) revert TransferFailed(asset_, to_, amount_);
if (amount_ > available(asset_)) {
revert InsufficientFunds(asset_, available(asset_), amount_);
}

_distributedFungible[asset_] += amount_;

// Transfer the asset to the recipient
if (asset_ == address(0)) {
SafeTransferLib.safeTransferETH(to_, amount_);
} else {
asset_.safeTransfer(to_, amount_);
}

emit Distributed(asset_, to_, amount_);
}

/// @notice Calculate the portion of allocated assets vested at a given timestamp
/// @param asset_ The address of the asset
/// @param timestamp_ The timestamp used to calculate the vested amount
/// @return The amount of assets vested at that point in time
function _vestedAllocation(address asset_, uint64 timestamp_) internal view virtual returns (uint256) {
uint256 balance = asset_ == address(0) ? address(this).balance : asset_.balanceOf(address(this));
return _linearVestedAmount(balance + _distributedFungible[asset_], timestamp_);
}

/// @notice Calculate the amount of assets vested at a given timestamp using a linear vesting schedule
/// @param totalAllocation The total amount of the asset allocated to the budget (including prior distributions)
/// @param timestamp The timestamp used to calculate the vested amount
/// @return The amount of assets vested at that point in time
function _linearVestedAmount(uint256 totalAllocation, uint64 timestamp) internal view virtual returns (uint256) {
if (timestamp < start + cliff) {
return 0;
} else if (timestamp >= start + duration) {
return totalAllocation;
} else {
return totalAllocation * (timestamp - start) / duration;
}
}
}
Loading

0 comments on commit 810f612

Please sign in to comment.