Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

first pass #39

Closed
wants to merge 19 commits into from
2 changes: 1 addition & 1 deletion lib/sphinx
Submodule sphinx updated 107 files
134 changes: 127 additions & 7 deletions src/JBBuybackHook.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {IJBPrices} from "@bananapus/core/src/interfaces/IJBPrices.sol";
import {IJBProjects} from "@bananapus/core/src/interfaces/IJBProjects.sol";
import {IJBRulesetDataHook} from "@bananapus/core/src/interfaces/IJBRulesetDataHook.sol";
import {IJBTerminal} from "@bananapus/core/src/interfaces/IJBTerminal.sol";
import {IJBToken} from "@bananapus/core/src/interfaces/IJBToken.sol";
import {JBConstants} from "@bananapus/core/src/libraries/JBConstants.sol";
import {JBMetadataResolver} from "@bananapus/core/src/libraries/JBMetadataResolver.sol";
import {JBRulesetMetadataResolver} from "@bananapus/core/src/libraries/JBRulesetMetadataResolver.sol";
Expand All @@ -31,6 +32,8 @@ import {JBPermissionIds} from "@bananapus/permission-ids/src/JBPermissionIds.sol

import {IJBBuybackHook} from "./interfaces/IJBBuybackHook.sol";
import {IWETH9} from "./interfaces/external/IWETH9.sol";
import {JBVestedBuybackClaims} from "./structs/JBVestedBuybackClaims.sol";
import {JBVestingBuyback} from "./structs/JBVestingBuyback.sol";

/// @custom:benediction DEVS BENEDICAT ET PROTEGAT CONTRACTVS MEAM
/// @notice The buyback hook allows beneficiaries of a payment to a project to either:
Expand All @@ -52,6 +55,7 @@ contract JBBuybackHook is JBPermissioned, IJBBuybackHook {

error JBBuybackHook_CallerNotPool(address caller);
error JBBuybackHook_InsufficientPayAmount(uint256 swapAmount, uint256 totalPaid);
error JBBuybackHook_IndexOutOfBounds(uint256 index, uint256 numberOfVestingBuybacks);
error JBBuybackHook_InvalidTwapSlippageTolerance(uint256 value, uint256 min, uint256 max);
error JBBuybackHook_InvalidTwapWindow(uint256 value, uint256 min, uint256 max);
error JBBuybackHook_PoolAlreadySet(IUniswapV3Pool pool);
Expand Down Expand Up @@ -85,6 +89,9 @@ contract JBBuybackHook is JBPermissioned, IJBBuybackHook {
/// @notice The denominator used when calculating TWAP slippage percent values.
uint256 public constant override TWAP_SLIPPAGE_DENOMINATOR = 10_000;

/// @notice The duration of the vesting period.
uint256 public constant override VESTING_PERIOD = 180 days;

//*********************************************************************//
// -------------------- public immutable properties ------------------ //
//*********************************************************************//
Expand Down Expand Up @@ -131,6 +138,11 @@ contract JBBuybackHook is JBPermissioned, IJBBuybackHook {
/// @custom:param projectId The ID of the project to get the twap parameters for.
mapping(uint256 projectId => uint256) internal _twapParamsOf;

/// @notice The buybacks that are vesting to each beneficiary.
/// @custom:param token The token which the buybacks apply to.
/// @custom:param beneficiary The address which the buybacks belong to.
mapping(address token => mapping(address beneficiary => JBVestingBuyback[])) internal _vestingBuybacksFor;

//*********************************************************************//
// ---------------------------- constructor -------------------------- //
//*********************************************************************//
Expand Down Expand Up @@ -256,7 +268,10 @@ contract JBBuybackHook is JBPermissioned, IJBBuybackHook {
hook: IJBPayHook(this),
amount: amountToSwapWith,
metadata: abi.encode(
projectTokenIs0, totalPaid == amountToSwapWith ? 0 : totalPaid - amountToSwapWith, minimumSwapAmountOut
projectTokenIs0,
totalPaid == amountToSwapWith ? 0 : totalPaid - amountToSwapWith,
minimumSwapAmountOut,
projectToken
)
});

Expand Down Expand Up @@ -386,11 +401,13 @@ contract JBBuybackHook is JBPermissioned, IJBBuybackHook {
}

// Parse the metadata forwarded from the data hook.
(bool projectTokenIs0, uint256 amountToMintWith, uint256 minimumSwapAmountOut) =
abi.decode(context.hookMetadata, (bool, uint256, uint256));
(bool projectTokenIs0, uint256 amountToMintWith, uint256 minimumSwapAmountOut, address token) =
abi.decode(context.hookMetadata, (bool, uint256, uint256, address));

// If the token paid in isn't the native token, pull the amount to swap from the terminal.
uint256 balanceBefore = 0;
if (context.forwardedAmount.token != JBConstants.NATIVE_TOKEN) {
balanceBefore = IERC20(context.forwardedAmount.token).balanceOf(address(this));
IERC20(context.forwardedAmount.token).safeTransferFrom(
msg.sender, address(this), context.forwardedAmount.value
);
Expand All @@ -408,7 +425,7 @@ contract JBBuybackHook is JBPermissioned, IJBBuybackHook {
// Get a reference to any terminal tokens which were paid in and are still held by this contract.
uint256 leftoverAmountInThisContract = context.forwardedAmount.token == JBConstants.NATIVE_TOKEN
? address(this).balance
: IERC20(context.forwardedAmount.token).balanceOf(address(this));
: IERC20(context.forwardedAmount.token).balanceOf(address(this)) - balanceBefore;

// Get a reference to the ruleset.
// slither-disable-next-line unused-return
Expand Down Expand Up @@ -465,16 +482,56 @@ contract JBBuybackHook is JBPermissioned, IJBBuybackHook {
// Add the amount to mint to the leftover mint amount.
partialMintTokenCount += mulDiv(amountToMintWith, context.weight, weightRatio);

// Mint the calculated amount of tokens for the beneficiary, including any leftover amount.
// Mint the total amount of tokens to be vested to this contract.
// This takes the reserved rate into account.
// slither-disable-next-line unused-return
CONTROLLER.mintTokensOf({
uint256 amountToVest = CONTROLLER.mintTokensOf({
projectId: context.projectId,
tokenCount: exactSwapAmountOut + partialMintTokenCount,
beneficiary: address(context.beneficiary),
beneficiary: address(this),
memo: "",
useReservedPercent: true
});

// Keep a reference to the array of vesting buybacks for the beneficiary.
JBVestingBuyback[] storage vestingBuybacks = _vestingBuybacksFor[token][context.beneficiary];

// Compute the end time of the vesting period.
uint256 endsAt = block.timestamp + VESTING_PERIOD;

// Add the calculated amount of tokens to be vested for the beneficiary, including any leftover amount.
// This takes the reserved rate into account.
// slither-disable-next-line unused-return
vestingBuybacks.push(
JBVestingBuyback({
amount: uint160(amountToVest),
lastClaimedAt: uint48(block.timestamp),
endsAt: uint48(endsAt)
})
);

emit StartVestingBuyback({
projectId: context.projectId,
beneficiary: context.beneficiary,
index: vestingBuybacks.length - 1,
amount: amountToVest,
startsAt: block.timestamp,
endsAt: endsAt,
caller: msg.sender
});
}

/// @notice Claim multiple vested buybacks for multiple beneficiaries.
/// @param claims An array of `JBVestedBuybackClaims` structs, each representing a buyback to claim.
function claimVestedBuybacksFor(JBVestedBuybackClaims[] calldata claims) external {
// Keep a reference to the buyback being iterated on.
JBVestedBuybackClaims memory claim;

// Iterate over the claims and mint the tokens for the beneficiary.
for (uint256 i; i < claims.length; i++) {
claim = claims[i];
claimVestedBuybacksFor({token: claim.token, beneficiary: claim.beneficiary, indices: claim.indices});
}
}

/// @notice Set the pool to use for a given project and terminal token (the default for the project's token <->
Expand Down Expand Up @@ -674,6 +731,69 @@ contract JBBuybackHook is JBPermissioned, IJBBuybackHook {
IERC20(terminalTokenWithWETH).safeTransfer(msg.sender, amountToSendToPool);
}

//*********************************************************************//
// ----------------------- public transactions ----------------------- //
//*********************************************************************//

/// @notice Claim all vested buybacks for a beneficiary.
/// @param token The token to claim the vested buybacks of.
/// @param beneficiary The address which the buybacks belong to.
/// @param indices The indices of the buybacks to claim.
/// @return amount The total number of tokens claimed.
function claimVestedBuybacksFor(
address token,
address beneficiary,
uint256[] memory indices
)
public
returns (uint256 amount)
{
// Get a reference to the number of buybacks for the beneficiary.
uint256 numberOfBuybacks = _vestingBuybacksFor[token][beneficiary].length;

// Keep a reference to the buyback being iterated on.
JBVestingBuyback memory buyback;

// Iterate over the buybacks and sum the vested amounts.
for (uint256 i; i < indices.length; i++) {
// Get a reference to the index of the buyback to claim.
uint256 index = indices[i];

// Make sure the index is within bounds.
if (index >= numberOfBuybacks) revert JBBuybackHook_IndexOutOfBounds(index, numberOfBuybacks);

// Get a reference to the buyback.
buyback = _vestingBuybacksFor[token][beneficiary][index];

// If the buyback has no amount, skip it.
if (buyback.amount == 0) continue;

// Get a reference to the vested amount.
uint256 vestedAmount = block.timestamp >= buyback.endsAt
? buyback.amount
: mulDiv(buyback.amount, block.timestamp - buyback.lastClaimedAt, buyback.endsAt - buyback.lastClaimedAt);

// Add the vested amount to the total amount claimed.
amount += vestedAmount;

// If the buyback hasn't been fully vested, update the buyback's amount and last claimed at.
_vestingBuybacksFor[token][beneficiary][index].amount -= uint160(vestedAmount);
_vestingBuybacksFor[token][beneficiary][index].lastClaimedAt = uint48(block.timestamp);

emit ClaimVestedBuybacks({
token: token,
beneficiary: beneficiary,
index: index,
amountVested: vestedAmount,
amountLeft: buyback.amount - uint160(vestedAmount),
caller: msg.sender
});
}

// Transfer the tokens to the beneficiary.
IERC20(address(token)).safeTransfer({to: beneficiary, value: amount});
}

//*********************************************************************//
// ---------------------- internal functions ------------------------- //
//*********************************************************************//
Expand Down
28 changes: 28 additions & 0 deletions src/interfaces/IJBBuybackHook.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,40 @@ import {IJBPayHook} from "@bananapus/core/src/interfaces/IJBPayHook.sol";
import {IJBPrices} from "@bananapus/core/src/interfaces/IJBPrices.sol";
import {IJBProjects} from "@bananapus/core/src/interfaces/IJBProjects.sol";
import {IJBRulesetDataHook} from "@bananapus/core/src/interfaces/IJBRulesetDataHook.sol";
import {IJBToken} from "@bananapus/core/src/interfaces/IJBToken.sol";
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import {IUniswapV3SwapCallback} from "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol";

import {IWETH9} from "./external/IWETH9.sol";
import {JBVestedBuybackClaims} from "../structs/JBVestedBuybackClaims.sol";

interface IJBBuybackHook is IJBPayHook, IJBRulesetDataHook, IUniswapV3SwapCallback {
event Swap(
uint256 indexed projectId, uint256 amountToSwapWith, IUniswapV3Pool pool, uint256 amountReceived, address caller
);
event Mint(uint256 indexed projectId, uint256 leftoverAmount, uint256 tokenCount, address caller);
event PoolAdded(uint256 indexed projectId, address indexed terminalToken, address pool, address caller);
event StartVestingBuyback(
uint256 indexed projectId,
address indexed beneficiary,
uint256 indexed index,
uint256 amount,
uint256 startsAt,
uint256 endsAt,
address caller
);
event TwapWindowChanged(uint256 indexed projectId, uint256 oldWindow, uint256 newWindow, address caller);
event TwapSlippageToleranceChanged(
uint256 indexed projectId, uint256 oldTolerance, uint256 newTolerance, address caller
);
event ClaimVestedBuybacks(
address indexed token,
address indexed beneficiary,
uint256 indexed index,
uint256 amountVested,
uint256 amountLeft,
address caller
);

function CONTROLLER() external view returns (IJBController);
function DIRECTORY() external view returns (IJBDirectory);
Expand All @@ -34,12 +53,21 @@ interface IJBBuybackHook is IJBPayHook, IJBRulesetDataHook, IUniswapV3SwapCallba
function PROJECTS() external view returns (IJBProjects);
function UNISWAP_V3_FACTORY() external view returns (address);
function WETH() external view returns (IWETH9);
function VESTING_PERIOD() external view returns (uint256);

function poolOf(uint256 projectId, address terminalToken) external view returns (IUniswapV3Pool pool);
function projectTokenOf(uint256 projectId) external view returns (address projectTokenOf);
function twapSlippageToleranceOf(uint256 projectId) external view returns (uint256 slippageTolerance);
function twapWindowOf(uint256 projectId) external view returns (uint32 window);

function claimVestedBuybacksFor(JBVestedBuybackClaims[] calldata claims) external;
function claimVestedBuybacksFor(
address token,
address beneficiary,
uint256[] calldata indices
)
external
returns (uint256 amount);
function setPoolFor(
uint256 projectId,
uint24 fee,
Expand Down
13 changes: 13 additions & 0 deletions src/structs/JBVestedBuybackClaims.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IJBToken} from "@bananapus/core/src/interfaces/IJBToken.sol";

/// @custom:member token The token to claim the vested buybacks of.
/// @custom:member beneficiary The address which the buybacks belong to.
/// @custom:member indices The indices of the buybacks to claim.
struct JBVestedBuybackClaims {
address token;
address beneficiary;
uint256[] indices;
}
11 changes: 11 additions & 0 deletions src/structs/JBVestingBuyback.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/// @custom:member amount The amount of tokens to be streamed to the beneficiary.
/// @custom:member lastClaimedAt The time at which the vested tokens were last claimed.
/// @custom:member endTime The end time of the vesting period.
struct JBVestingBuyback {
uint160 amount;
uint48 lastClaimedAt;
uint48 endsAt;
}
34 changes: 20 additions & 14 deletions test/Fork.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -380,12 +380,13 @@ contract TestJBBuybackHook_Fork is TestBaseWorkflow, JBTest, UniswapV3ForgeQuote
);

// Check: token received by the multisig()
assertApproxEqAbs(
jbx.balanceOf(multisig()) - _balBeforePayment,
_amountOutQuoted - (_amountOutQuoted * _reservedPercent / 10_000),
1,
"wrong balance"
);
// rm vesting:assertApproxEqAbs(
// jbx.balanceOf(multisig()) - _balBeforePayment,
// _amountOutQuoted - (_amountOutQuoted * _reservedPercent / 10_000),
// 1,
// "wrong balance"
// );
assertApproxEqAbs(jbx.balanceOf(multisig()) - _balBeforePayment, 0, 1, "wrong balance");

// Check: token added to the reserve - 1 wei sensitivity for rounding errors
assertApproxEqAbs(
Expand Down Expand Up @@ -467,7 +468,8 @@ contract TestJBBuybackHook_Fork is TestBaseWorkflow, JBTest, UniswapV3ForgeQuote
);

// Check: token received by the multisig()
assertEq(jbx.balanceOf(multisig()), _balanceBene + amountOutQuoted / 2);
// rm vesting:assertEq(jbx.balanceOf(multisig()), _balanceBene + amountOutQuoted / 2);
assertEq(jbx.balanceOf(multisig()), 0);

// Check: token added to the reserve - 1 wei sensitivity for rounding errors
assertApproxEqAbs(jbController().pendingReservedTokenBalanceOf(1), _reserveBalance + amountOutQuoted / 2, 1);
Expand Down Expand Up @@ -523,7 +525,8 @@ contract TestJBBuybackHook_Fork is TestBaseWorkflow, JBTest, UniswapV3ForgeQuote
uint256 _diff = _balAfterPayment - _balBeforePayment;

// Check: token received by the multisig()
assertEq(_diff, _quote);
// rm vesting: assertEq(_diff, _quote);
assertEq(_diff, 0);

// Check: reserve unchanged
assertEq(jbController().pendingReservedTokenBalanceOf(1), _reservedBalanceBefore);
Expand Down Expand Up @@ -579,7 +582,9 @@ contract TestJBBuybackHook_Fork is TestBaseWorkflow, JBTest, UniswapV3ForgeQuote
// 1 wei sensitivity for rounding errors
if (_twap > _tokenCount) {
// Path is picked based on twap, but the token received are the one quoted
assertApproxEqAbs(_tokenReceived, _quote - (_quote * _reservedPercent) / 10_000, 1, "wrong swap");
// rm for vesting: assertApproxEqAbs(_tokenReceived, _quote - (_quote * _reservedPercent) / 10_000, 1,
// "wrong swap");
assertApproxEqAbs(_tokenReceived, 0, 1, "wrong swap");
assertApproxEqAbs(
jbController().pendingReservedTokenBalanceOf(1),
_reservedBalanceBefore + (_quote * _reservedPercent) / 10_000,
Expand Down Expand Up @@ -725,11 +730,12 @@ contract TestJBBuybackHook_Fork is TestBaseWorkflow, JBTest, UniswapV3ForgeQuote
);

// Check: token received by the multisig()
assertApproxEqAbs(
jbx.balanceOf(multisig()) - _balBeforePayment,
amountOutQuoted / 2 + mulDiv18(_amountInExtra, _weight) / 2,
10
);
// rm vesting: assertApproxEqAbs(
// jbx.balanceOf(multisig()) - _balBeforePayment,
// amountOutQuoted / 2 + mulDiv18(_amountInExtra, _weight) / 2,
// 10
// );
assertApproxEqAbs(jbx.balanceOf(multisig()) - _balBeforePayment, 0, 10);

// Check: token added to the reserve
assertApproxEqAbs(
Expand Down
Loading
Loading