Skip to content

Commit

Permalink
Merge pull request #20 from simplemachine92/experiment/exact-swap-out
Browse files Browse the repository at this point in the history
  • Loading branch information
simon-something authored Sep 28, 2023
2 parents ad2d40c + 1431a5d commit 93cd18b
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 32 deletions.
25 changes: 16 additions & 9 deletions contracts/JBBuybackDelegate.sol
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ contract JBBuybackDelegate is ERC165, JBOperatable, IJBBuybackDelegate {
override
returns (uint256 weight, string memory memo, JBPayDelegateAllocation3_1_1[] memory delegateAllocations)
{
// Keep a reference to the payment total
uint256 _totalPaid = _data.amount.value;

// Keep a reference to the weight
uint256 _weight = _data.weight;

// Keep a reference to the minimum number of tokens expected to be swapped for.
uint256 _minimumSwapAmountOut;

Expand All @@ -161,10 +167,11 @@ contract JBBuybackDelegate is ERC165, JBOperatable, IJBBuybackDelegate {
}

// If no amount was specified to swap with, default to the full amount of the payment.
if (_amountToSwapWith == 0) _amountToSwapWith = _data.amount.value;
if (_amountToSwapWith == 0) _amountToSwapWith = _totalPaid;

// Find the default total number of tokens to mint as if no Buyback Delegate were installed, as a fixed point number with 18 decimals
uint256 _tokenCountWithoutDelegate = mulDiv(_amountToSwapWith, _data.weight, _data.amount.decimals);

uint256 _tokenCountWithoutDelegate = mulDiv(_amountToSwapWith, _weight, 10 ** _data.amount.decimals);

// Keep a reference to the project's token.
address _projectToken = projectTokenOf[_data.projectId];
Expand All @@ -180,7 +187,7 @@ contract JBBuybackDelegate is ERC165, JBOperatable, IJBBuybackDelegate {
// If the minimum amount received from swapping is greather than received when minting, use the swap path.
if (_tokenCountWithoutDelegate < _minimumSwapAmountOut) {
// Make sure the amount to swap with is at most the full amount being paid.
if (_amountToSwapWith > _data.amount.value) revert JuiceBuyback_InsufficientPayAmount();
if (_amountToSwapWith > _totalPaid) revert JuiceBuyback_InsufficientPayAmount();

// Keep a reference to a flag indicating if the pool will reference the project token as the first in the pair.
bool _projectTokenIs0 = address(_projectToken) < _terminalToken;
Expand All @@ -190,7 +197,7 @@ contract JBBuybackDelegate is ERC165, JBOperatable, IJBBuybackDelegate {
delegateAllocations[0] = JBPayDelegateAllocation3_1_1({
delegate: IJBPayDelegate3_1_1(this),
amount: _amountToSwapWith,
metadata: abi.encode(_quoteExists, _projectTokenIs0, _minimumSwapAmountOut, _data.amount.value - _amountToSwapWith, _data.weight)
metadata: abi.encode(_quoteExists, _projectTokenIs0, _minimumSwapAmountOut, _totalPaid == _amountToSwapWith ? 0 : _totalPaid - _amountToSwapWith, _weight)
});

// All the mint will be done in didPay, return 0 as weight to avoid minting via the terminal
Expand Down Expand Up @@ -263,7 +270,7 @@ contract JBBuybackDelegate is ERC165, JBOperatable, IJBBuybackDelegate {
uint256 _minimumSwapAmountOut,
uint256 _amountToMintWith,
uint256 _weight
) = abi.decode(_data.dataSourceMetadata, (bool, bool, uint256, uint256));
) = abi.decode(_data.dataSourceMetadata, (bool, bool, uint256, uint256, uint256));

// Get a reference to the amount of tokens that was swapped for.
uint256 _exactSwapAmountOut = _swap(_data, _projectTokenIs0);
Expand All @@ -280,7 +287,7 @@ contract JBBuybackDelegate is ERC165, JBOperatable, IJBBuybackDelegate {
// Keep a reference to the number of tokens being minted.
uint256 _partialMintTokenCount;
if (_terminalTokenInThisContract != 0) {
_partialMintTokenCount = mulDiv(_terminalTokenInThisContract, _weight, _data.amount.decimals);
_partialMintTokenCount = mulDiv(_terminalTokenInThisContract, _weight, 10 ** _data.amount.decimals);

// If the token paid in wasn't ETH, give the terminal permission to pull them back into its balance.
if (_data.forwardedAmount.token != JBTokens.ETH) {
Expand All @@ -295,13 +302,13 @@ contract JBBuybackDelegate is ERC165, JBOperatable, IJBBuybackDelegate {
emit BuybackDelegate_Mint(_data.projectId, _terminalTokenInThisContract, _partialMintTokenCount, msg.sender);
}

// Get a reference to any amount of token paid which are in the terminal's balance (ie extra-funds used to mint)
uint256 _tokensToMintFromTerminal = mulDiv(_amountToMintWith, _weight, _data.amount.decimals);
// Add amount to mint to leftover mint amount (avoiding stack too deep here)
_partialMintTokenCount += mulDiv(_amountToMintWith, _weight, 10 ** _data.amount.decimals);

// Mint the whole amount of tokens again together with the (optional partial mint), such that the correct portion of reserved tokens get taken into account.
CONTROLLER.mintTokensOf({
projectId: _data.projectId,
tokenCount: _exactSwapAmountOut + _partialMintTokenCount + _tokensToMintFromTerminal,
tokenCount: _exactSwapAmountOut + _partialMintTokenCount,
beneficiary: address(_data.beneficiary),
memo: _data.memo,
preferClaimedTokens: _data.preferClaimedTokens,
Expand Down
2 changes: 2 additions & 0 deletions contracts/test/JBBuybackDelegate_Fork.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import "@exhausted-pigeon/uniswap-v3-forge-quoter/src/UniswapV3ForgeQuoter.sol";

import "../JBBuybackDelegate.sol";

import {mulDiv18} from "@prb/math/src/Common.sol";

/**
* @notice Buyback fork integration tests, using $jbx v3
*/
Expand Down
72 changes: 49 additions & 23 deletions contracts/test/JBBuybackDelegate_Unit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,20 @@ contract TestJBBuybackDelegate_Units is Test {
*
* @dev _tokenCount == weight, as we use a value of 1.
*/
function test_payParams_callWithQuote(uint256 _weight, uint256 _swapOutCount, uint256 _amountIn) public {
function test_payParams_callWithQuote(uint256 _weight, uint256 _swapOutCount, uint256 _amountIn, uint256 _decimals) public {
// Avoid accidentally using the twap (triggered if out == 0)
_swapOutCount = bound(_swapOutCount, 1, type(uint256).max);

// Avoid mulDiv overflow
_weight = bound(_weight, 1, 1 ether);

// Use between 1 wei and the whole amount from pay(..)
_amountIn = bound(_amountIn, 1, payParams.amount.value);

// Avoid accidentally using the twap (triggered if out == 0)
_swapOutCount = bound(_swapOutCount, 1, type(uint256).max);
// The terminal token decimals
_decimals = bound(_decimals, 1, 18);

uint256 _tokenCount = mulDiv18(_amountIn, _weight);
uint256 _tokenCount = mulDiv(_amountIn, _weight, 10**_decimals);

// Pass the quote as metadata
bytes[] memory _data = new bytes[](1);
Expand All @@ -164,6 +170,7 @@ contract TestJBBuybackDelegate_Units is Test {
// Set the relevant payParams data
payParams.weight = _weight;
payParams.metadata = _metadata;
payParams.amount = JBTokenAmount({token: address(weth), value: 1 ether, decimals: _decimals, currency: 1});

// Returned values to catch:
JBPayDelegateAllocation3_1_1[] memory _allocationsReturned;
Expand All @@ -189,7 +196,7 @@ contract TestJBBuybackDelegate_Units is Test {
assertEq(_allocationsReturned[0].amount, _amountIn, "worng amount in returned");
assertEq(
_allocationsReturned[0].metadata,
abi.encode(true, address(projectToken) < address(weth), _swapOutCount, payParams.weight),
abi.encode(true, address(projectToken) < address(weth), _swapOutCount, payParams.amount.value - _amountIn, payParams.weight),
"wrong metadata"
);

Expand Down Expand Up @@ -267,7 +274,7 @@ contract TestJBBuybackDelegate_Units is Test {
assertEq(
_allocationsReturned[0].metadata,
abi.encode(
false, address(projectToken) < address(weth), _twapAmountOut, payParams.weight
false, address(projectToken) < address(weth), _twapAmountOut, 0, payParams.weight
),
"wrong metadata"
);
Expand Down Expand Up @@ -321,7 +328,7 @@ contract TestJBBuybackDelegate_Units is Test {

uint256 _weight = 1 ether;

uint256 _tokenCount = mulDiv18(_amountIn, _weight);
uint256 _tokenCount = mulDiv(_amountIn, _weight, 10**18);

// Avoid accidentally using the twap (triggered if out == 0)
_swapOutCount = bound(_swapOutCount, _tokenCount + 1, type(uint256).max);
Expand Down Expand Up @@ -366,6 +373,7 @@ contract TestJBBuybackDelegate_Units is Test {
true, // use quote
address(projectToken) < address(weth),
_tokenCount,
0,
_twapQuote
);

Expand Down Expand Up @@ -538,22 +546,25 @@ contract TestJBBuybackDelegate_Units is Test {
/**
* @notice Test didPay with token received from swapping
*/
function test_didPay_swap_ERC20(uint256 _tokenCount, uint256 _twapQuote ) public {
function test_didPay_swap_ERC20(uint256 _tokenCount, uint256 _twapQuote, uint256 _decimals ) public {
// Bound to avoid overflow and insure swap quote > mint quote
_tokenCount = bound(_tokenCount, 2, type(uint256).max - 1);
_twapQuote = bound(_twapQuote, _tokenCount + 1, type(uint256).max);

_decimals = bound(_decimals, 1, 18);

didPayData.amount =
JBTokenAmount({token: address(randomTerminalToken), value: 1 ether, decimals: 18, currency: 1});
JBTokenAmount({token: address(randomTerminalToken), value: 1 ether, decimals: _decimals, currency: 1});
didPayData.forwardedAmount =
JBTokenAmount({token: address(randomTerminalToken), value: 1 ether, decimals: 18, currency: 1});
JBTokenAmount({token: address(randomTerminalToken), value: 1 ether, decimals: _decimals, currency: 1});
didPayData.projectId = randomId;

// The metadata coming from payParams(..)
didPayData.dataSourceMetadata = abi.encode(
true, // use quote
address(projectToken) < address(weth),
_tokenCount,
0,
_twapQuote
);

Expand Down Expand Up @@ -654,6 +665,7 @@ contract TestJBBuybackDelegate_Units is Test {
true, // use quote
address(projectToken) < address(weth),
_tokenCount,
0,
1 ether // weight - unused
);

Expand Down Expand Up @@ -693,17 +705,23 @@ contract TestJBBuybackDelegate_Units is Test {
/**
* @notice Test didPay with swap reverting while using the twap, should then mint with the delegate balance, random erc20 is terminal token
*/
function test_didPay_swapRevertWithoutQuote_ERC20(uint256 _tokenCount, uint256 _weight) public {
function test_didPay_swapRevertWithoutQuote_ERC20(uint256 _tokenCount, uint256 _weight, uint256 _decimals, uint256 _extraMint) public {
// The current weight
_weight = bound(_weight, 1, 1 ether);

// The amount of termminal token in this delegate (avoid overflowing when mul by weight)
_tokenCount = bound(_tokenCount, 2, type(uint128).max);

// An extra amount of token to mint, based on fund which stayed in the terminal
_extraMint = bound(_extraMint, 2, type(uint128).max);

// The terminal token decimal
_decimals = bound(_decimals, 1, 18);

didPayData.amount =
JBTokenAmount({token: address(randomTerminalToken), value: _tokenCount, decimals: 18, currency: 1});
JBTokenAmount({token: address(randomTerminalToken), value: _tokenCount, decimals: _decimals, currency: 1});
didPayData.forwardedAmount =
JBTokenAmount({token: address(randomTerminalToken), value: _tokenCount, decimals: 18, currency: 1});
JBTokenAmount({token: address(randomTerminalToken), value: _tokenCount, decimals: _decimals, currency: 1});
didPayData.projectId = randomId;

vm.mockCall(
Expand All @@ -717,6 +735,7 @@ contract TestJBBuybackDelegate_Units is Test {
false, // use quote
address(otherRandomProjectToken) < address(randomTerminalToken),
_tokenCount,
_extraMint, // extra amount to mint with
_weight
);

Expand Down Expand Up @@ -763,15 +782,15 @@ contract TestJBBuybackDelegate_Units is Test {
address(controller),
abi.encodeCall(
controller.mintTokensOf,
(didPayData.projectId, mulDiv18(_tokenCount, _weight), didPayData.beneficiary, didPayData.memo, didPayData.preferClaimedTokens, true)
(didPayData.projectId, mulDiv(_tokenCount, _weight, 10**_decimals) + mulDiv(_extraMint, _weight, 10**_decimals), didPayData.beneficiary, didPayData.memo, didPayData.preferClaimedTokens, true)
),
abi.encode(true)
);
vm.expectCall(
address(controller),
abi.encodeCall(
controller.mintTokensOf,
(didPayData.projectId, _tokenCount * _weight / 1e18, didPayData.beneficiary, didPayData.memo, didPayData.preferClaimedTokens, true)
(didPayData.projectId, mulDiv(_tokenCount, _weight, 10**_decimals) + mulDiv(_extraMint, _weight, 10**_decimals), didPayData.beneficiary, didPayData.memo, didPayData.preferClaimedTokens, true)
)
);

Expand Down Expand Up @@ -802,9 +821,9 @@ contract TestJBBuybackDelegate_Units is Test {
)
);

// expect event
// expect event - only for the non-extra mint
vm.expectEmit(true, true, true, true);
emit BuybackDelegate_Mint(didPayData.projectId, _tokenCount, _tokenCount * _weight / 10**18, address(jbxTerminal));
emit BuybackDelegate_Mint(didPayData.projectId, _tokenCount, mulDiv(_tokenCount, _weight, 10**_decimals), address(jbxTerminal));

vm.prank(address(jbxTerminal));
delegate.didPay(didPayData);
Expand All @@ -813,24 +832,31 @@ contract TestJBBuybackDelegate_Units is Test {
/**
* @notice Test didPay with swap reverting while using the twap, should then mint with the delegate balance, random erc20 is terminal token
*/
function test_didPay_swapRevertWithoutQuote_ETH(uint256 _tokenCount, uint256 _weight) public {
function test_didPay_swapRevertWithoutQuote_ETH(uint256 _tokenCount, uint256 _weight, uint256 _decimals, uint256 _extraMint) public {
// The current weight
_weight = bound(_weight, 1, 1 ether);

// The amount of termminal token in this delegate (avoid overflowing when mul by weight)
_tokenCount = bound(_tokenCount, 2, type(uint128).max);

// An extra amount of token to mint, based on fund which stayed in the terminal
_extraMint = bound(_extraMint, 2, type(uint128).max);

// The terminal token decimal
_decimals = bound(_decimals, 1, 18);

didPayData.amount =
JBTokenAmount({token: JBTokens.ETH, value: _tokenCount, decimals: 18, currency: 1});
JBTokenAmount({token: JBTokens.ETH, value: _tokenCount, decimals: _decimals, currency: 1});

didPayData.forwardedAmount =
JBTokenAmount({token: JBTokens.ETH, value: _tokenCount, decimals: 18, currency: 1});
JBTokenAmount({token: JBTokens.ETH, value: _tokenCount, decimals: _decimals, currency: 1});

// The metadata coming from payParams(..)
didPayData.dataSourceMetadata = abi.encode(
false, // use quote
address(projectToken) < address(weth),
_tokenCount,
_extraMint,
_weight
);

Expand Down Expand Up @@ -871,15 +897,15 @@ contract TestJBBuybackDelegate_Units is Test {
address(controller),
abi.encodeCall(
controller.mintTokensOf,
(didPayData.projectId, mulDiv18(_tokenCount, _weight), didPayData.beneficiary, didPayData.memo, didPayData.preferClaimedTokens, true)
(didPayData.projectId, mulDiv(_tokenCount, _weight, 10**_decimals) + mulDiv(_extraMint, _weight, 10**_decimals), didPayData.beneficiary, didPayData.memo, didPayData.preferClaimedTokens, true)
),
abi.encode(true)
);
vm.expectCall(
address(controller),
abi.encodeCall(
controller.mintTokensOf,
(didPayData.projectId, _tokenCount * _weight / 1e18, didPayData.beneficiary, didPayData.memo, didPayData.preferClaimedTokens, true)
(didPayData.projectId, mulDiv(_tokenCount, _weight, 10**_decimals) + mulDiv(_extraMint, _weight, 10**_decimals), didPayData.beneficiary, didPayData.memo, didPayData.preferClaimedTokens, true)
)
);

Expand All @@ -904,7 +930,7 @@ contract TestJBBuybackDelegate_Units is Test {

// expect event
vm.expectEmit(true, true, true, true);
emit BuybackDelegate_Mint(didPayData.projectId, _tokenCount, _tokenCount * _weight / 10**18, address(jbxTerminal));
emit BuybackDelegate_Mint(didPayData.projectId, _tokenCount, mulDiv(_tokenCount, _weight, 10**_decimals), address(jbxTerminal));

vm.prank(address(jbxTerminal));
delegate.didPay(didPayData);
Expand Down

0 comments on commit 93cd18b

Please sign in to comment.