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

Implement deficit logic in BoringSolver #172

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 60 additions & 32 deletions src/base/Roles/BoringQueue/BoringSolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ contract BoringSolver is IBoringSolver, Auth, Multicall {
error BoringSolver___OnlySelf();
error BoringSolver___FailedToSolve();
error BoringSolver___OnlyQueue();

error BoringSolver___CannotCoverDeficit(uint256 deficit);
//============================== IMMUTABLES ===============================

BoringOnChainQueue internal immutable queue;
Expand Down Expand Up @@ -53,11 +53,12 @@ contract BoringSolver is IBoringSolver, Auth, Multicall {
/**
* @notice Solve multiple user requests to redeem Boring Vault shares.
*/
function boringRedeemSolve(BoringOnChainQueue.OnChainWithdraw[] calldata requests, address teller)
external
requiresAuth
{
bytes memory solveData = abi.encode(SolveType.BORING_REDEEM, msg.sender, teller, true);
function boringRedeemSolve(
BoringOnChainQueue.OnChainWithdraw[] calldata requests,
address teller,
bool coverDeficit
) external requiresAuth {
bytes memory solveData = abi.encode(SolveType.BORING_REDEEM, msg.sender, teller, true, coverDeficit);

queue.solveOnChainWithdraws(requests, solveData, address(this));
}
Expand All @@ -70,10 +71,12 @@ contract BoringSolver is IBoringSolver, Auth, Multicall {
BoringOnChainQueue.OnChainWithdraw[] calldata requests,
address fromTeller,
address toTeller,
address intermediateAsset
address intermediateAsset,
bool coverDeficit
) external requiresAuth {
bytes memory solveData =
abi.encode(SolveType.BORING_REDEEM_MINT, msg.sender, fromTeller, toTeller, intermediateAsset, true);
bytes memory solveData = abi.encode(
SolveType.BORING_REDEEM_MINT, msg.sender, fromTeller, toTeller, intermediateAsset, true, coverDeficit
);

queue.solveOnChainWithdraws(requests, solveData, address(this));
}
Expand All @@ -92,7 +95,7 @@ contract BoringSolver is IBoringSolver, Auth, Multicall {
BoringOnChainQueue.OnChainWithdraw[] memory requests = new BoringOnChainQueue.OnChainWithdraw[](1);
requests[0] = request;

bytes memory solveData = abi.encode(SolveType.BORING_REDEEM, msg.sender, teller, false);
bytes memory solveData = abi.encode(SolveType.BORING_REDEEM, msg.sender, teller, false, false);

queue.solveOnChainWithdraws(requests, solveData, address(this));
}
Expand All @@ -113,7 +116,7 @@ contract BoringSolver is IBoringSolver, Auth, Multicall {
requests[0] = request;

bytes memory solveData =
abi.encode(SolveType.BORING_REDEEM_MINT, msg.sender, fromTeller, toTeller, intermediateAsset, false);
abi.encode(SolveType.BORING_REDEEM_MINT, msg.sender, fromTeller, toTeller, intermediateAsset, false, false);

queue.solveOnChainWithdraws(requests, solveData, address(this));
}
Expand Down Expand Up @@ -159,25 +162,35 @@ contract BoringSolver is IBoringSolver, Auth, Multicall {
uint256 totalShares,
uint256 requiredAssets
) internal {
(, address solverOrigin, TellerWithMultiAssetSupport teller, bool excessToSolver) =
abi.decode(solveData, (SolveType, address, TellerWithMultiAssetSupport, bool));
(, address solverOrigin, TellerWithMultiAssetSupport teller, bool excessToSolver, bool coverDeficit) =
abi.decode(solveData, (SolveType, address, TellerWithMultiAssetSupport, bool, bool));

if (boringVault != address(teller.vault())) {
revert BoringSolver___BoringVaultTellerMismatch(boringVault, address(teller));
}

ERC20 asset = ERC20(solveAsset);
// Redeem the Boring Vault shares for Solve Asset.
uint256 assetsOut = teller.bulkWithdraw(asset, totalShares, requiredAssets, address(this));

// Transfer excess assets to solver origin or Boring Vault.
// Assets are sent to solver to cover gas fees.
// But if users are self solving, then the excess assets go to the Boring Vault.
if (excessToSolver) {
asset.safeTransfer(solverOrigin, assetsOut - requiredAssets);
} else {
asset.safeTransfer(boringVault, assetsOut - requiredAssets);
}
uint256 assetsOut = teller.bulkWithdraw(asset, totalShares, 0, address(this));

if (assetsOut > requiredAssets) {
// Transfer excess assets to solver origin or Boring Vault.
// Assets are sent to solver to cover gas fees.
// But if users are self solving, then the excess assets go to the Boring Vault.
if (excessToSolver) {
asset.safeTransfer(solverOrigin, assetsOut - requiredAssets);
} else {
asset.safeTransfer(boringVault, assetsOut - requiredAssets);
}
} else if (assetsOut < requiredAssets) {
// We have a deficit, cover it using solver origin funds if allowed.
uint256 deficit = requiredAssets - assetsOut;
if (coverDeficit) {
asset.safeTransferFrom(solverOrigin, address(this), deficit);
} else {
revert BoringSolver___CannotCoverDeficit(deficit);
}
} // else nothing to do, we have exact change.

// Approve Boring Queue to spend the required assets.
asset.approve(address(queue), requiredAssets);
Expand All @@ -199,9 +212,10 @@ contract BoringSolver is IBoringSolver, Auth, Multicall {
TellerWithMultiAssetSupport fromTeller,
TellerWithMultiAssetSupport toTeller,
ERC20 intermediateAsset,
bool excessToSolver
bool excessToSolver,
bool coverDeficit
) = abi.decode(
solveData, (SolveType, address, TellerWithMultiAssetSupport, TellerWithMultiAssetSupport, ERC20, bool)
solveData, (SolveType, address, TellerWithMultiAssetSupport, TellerWithMultiAssetSupport, ERC20, bool, bool)
);

if (fromBoringVault != address(fromTeller.vault())) {
Expand All @@ -220,9 +234,21 @@ contract BoringSolver is IBoringSolver, Auth, Multicall {
uint256 assetsToMintRequiredShares = requiredShares.mulDivUp(
toTeller.accountant().getRateInQuoteSafe(intermediateAsset), BoringOnChainQueue(queue).ONE_SHARE()
);

// Remove assetsToMintRequiredShares from excessAssets.
excessAssets = excessAssets - assetsToMintRequiredShares;
if (excessAssets > assetsToMintRequiredShares) {
// Remove assetsToMintRequiredShares from excessAssets.
excessAssets = excessAssets - assetsToMintRequiredShares;
} else if (excessAssets < assetsToMintRequiredShares) {
// We have a deficit, cover it using solver origin funds if allowed.
uint256 deficit = assetsToMintRequiredShares - excessAssets;
if (coverDeficit) {
intermediateAsset.safeTransferFrom(solverOrigin, address(this), deficit);
} else {
revert BoringSolver___CannotCoverDeficit(deficit);
}
excessAssets = 0;
} else {
excessAssets = 0;
}

// Approve toBoringVault to spend the Intermediate Asset.
intermediateAsset.safeApprove(toBoringVault, assetsToMintRequiredShares);
Expand All @@ -234,10 +260,12 @@ contract BoringSolver is IBoringSolver, Auth, Multicall {
// Transfer excess assets to solver origin or Boring Vault.
// Assets are sent to solver to cover gas fees.
// But if users are self solving, then the excess assets go to the from Boring Vault.
if (excessToSolver) {
intermediateAsset.safeTransfer(solverOrigin, excessAssets);
} else {
intermediateAsset.safeTransfer(fromBoringVault, excessAssets);
if (excessAssets > 0) {
if (excessToSolver) {
intermediateAsset.safeTransfer(solverOrigin, excessAssets);
} else {
intermediateAsset.safeTransfer(fromBoringVault, excessAssets);
}
}

// Approve Boring Queue to spend the required assets.
Expand Down
91 changes: 83 additions & 8 deletions test/BoringQueue.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,46 @@ contract BoringQueueTest is Test, MerkleTreeHelper {
// Solve users request using p2p solve.

uint256 wETHDelta = WETH.balanceOf(address(this));
boringSolver.boringRedeemSolve(requests, liquidEth_teller);
boringSolver.boringRedeemSolve(requests, liquidEth_teller, false);
wETHDelta = WETH.balanceOf(address(this)) - wETHDelta;

assertEq(WETH.balanceOf(testUser), requests[0].amountOfAssets, "User should have received their wETH.");
assertGt(wETHDelta, 0, "This address should have received some wETH.");
}

function testRedeemSolveCoverDeficit() external {
uint128 amountOfShares = 1_000e18;
uint16 sharePriceBpsDecrease = 2;
uint16 discount = 1;
uint24 secondsToDeadline = 1 days;
BoringOnChainQueue.OnChainWithdraw[] memory requests = new BoringOnChainQueue.OnChainWithdraw[](1);
(, requests[0]) = _haveUserCreateRequest(testUser, address(WETH), amountOfShares, discount, secondsToDeadline);

// Update liquidEth share price.
vm.startPrank(liquidEth_accountant.owner());
uint256 newRate = liquidEth_accountant.getRate();
newRate = newRate * (1e4 - sharePriceBpsDecrease) / 1e4;
liquidEth_accountant.updateExchangeRate(uint96(newRate));
vm.stopPrank();

skip(3 days);

uint256 expectedDeficit = 103525308149087000; // Pulled from logs of revert.
vm.expectRevert(
abi.encodeWithSelector(BoringSolver.BoringSolver___CannotCoverDeficit.selector, expectedDeficit)
);
boringSolver.boringRedeemSolve(requests, liquidEth_teller, false);

uint256 wETHDeficit = WETH.balanceOf(address(this));
WETH.approve(address(boringSolver), expectedDeficit);
boringSolver.boringRedeemSolve(requests, liquidEth_teller, true);

wETHDeficit = wETHDeficit - WETH.balanceOf(address(this));

assertEq(WETH.balanceOf(testUser), requests[0].amountOfAssets, "User should have received their wETH.");
assertEq(wETHDeficit, expectedDeficit, "Bad Deficit.");
}

function testRedeemMintSolve(uint128 amountOfShares, uint16 discount) external {
amountOfShares = uint128(bound(amountOfShares, 0.01e18, 1_000e18));
discount = uint16(bound(discount, 1, 100));
Expand All @@ -250,7 +283,7 @@ contract BoringQueueTest is Test, MerkleTreeHelper {
// Solve users request using p2p solve.

uint256 wETHDelta = WETH.balanceOf(address(this));
boringSolver.boringRedeemMintSolve(requests, liquidEth_teller, weETHs_teller, address(WETH));
boringSolver.boringRedeemMintSolve(requests, liquidEth_teller, weETHs_teller, address(WETH), false);
wETHDelta = WETH.balanceOf(address(this)) - wETHDelta;

assertEq(
Expand All @@ -259,6 +292,43 @@ contract BoringQueueTest is Test, MerkleTreeHelper {
assertGt(wETHDelta, 0, "This address should have received some wETH.");
}

function testRedeemMintSolveCoverDeficit() external {
uint128 amountOfShares = 1_000e18;
uint16 sharePriceBpsDecrease = 2;
uint16 discount = 1;
uint24 secondsToDeadline = 1 days;
BoringOnChainQueue.OnChainWithdraw[] memory requests = new BoringOnChainQueue.OnChainWithdraw[](1);
(, requests[0]) = _haveUserCreateRequest(testUser, weETHs, amountOfShares, discount, secondsToDeadline);

// Update liquidEth share price.
vm.startPrank(liquidEth_accountant.owner());
uint256 newRate = liquidEth_accountant.getRate();
newRate = newRate * (1e4 - sharePriceBpsDecrease) / 1e4;
liquidEth_accountant.updateExchangeRate(uint96(newRate));
vm.stopPrank();

// No need to skip since maturity is 0.

// Solve users request using p2p solve.

uint256 expectedDeficit = 103525308149085736; // Pulled from logs of revert.
vm.expectRevert(
abi.encodeWithSelector(BoringSolver.BoringSolver___CannotCoverDeficit.selector, expectedDeficit)
);
boringSolver.boringRedeemMintSolve(requests, liquidEth_teller, weETHs_teller, address(WETH), false);

uint256 wETHDeficit = WETH.balanceOf(address(this));
WETH.approve(address(boringSolver), expectedDeficit);
boringSolver.boringRedeemMintSolve(requests, liquidEth_teller, weETHs_teller, address(WETH), true);

wETHDeficit = wETHDeficit - WETH.balanceOf(address(this));

assertEq(
ERC20(weETHs).balanceOf(testUser), requests[0].amountOfAssets, "User should have received their weETHs."
);
assertEq(wETHDeficit, expectedDeficit, "Bad Deficit.");
}

function testUserRequestsThenCancels(uint128 amountOfShares, uint16 discount) external {
amountOfShares = uint128(bound(amountOfShares, 0.01e18, 1_000e18));
discount = uint16(bound(discount, 1, 100));
Expand Down Expand Up @@ -341,7 +411,7 @@ contract BoringQueueTest is Test, MerkleTreeHelper {
skip(3 days);

uint256 wETHDelta = WETH.balanceOf(address(this));
boringSolver.boringRedeemSolve(requests, liquidEth_teller);
boringSolver.boringRedeemSolve(requests, liquidEth_teller, false);
wETHDelta = WETH.balanceOf(address(this)) - wETHDelta;
uint256 endingShares = ERC20(liquidEth).balanceOf(testUser);

Expand Down Expand Up @@ -661,15 +731,15 @@ contract BoringQueueTest is Test, MerkleTreeHelper {
skip(3 days);

// Solve request using boringSolver.
boringSolver.boringRedeemSolve(requests, liquidEth_teller);
boringSolver.boringRedeemSolve(requests, liquidEth_teller, false);

// User makes a redeem mint solve request for weETHs.
address userB = vm.addr(3);
deal(address(liquidEth), userB, 1e18);
(, requests[0]) = _haveUserCreateRequest(userB, weETHs, 1e18, 100, 1 days);

// Solve request using boringSolver.
boringSolver.boringRedeemMintSolve(requests, liquidEth_teller, weETHs_teller, address(WETH));
boringSolver.boringRedeemMintSolve(requests, liquidEth_teller, weETHs_teller, address(WETH), false);

// User A and user B should not have any shares.
assertEq(ERC20(liquidEth).balanceOf(userA), 0, "User A should have had their shares solved.");
Expand Down Expand Up @@ -699,7 +769,12 @@ contract BoringQueueTest is Test, MerkleTreeHelper {
)
);
boringSolver.boringSolve(
address(boringSolver), liquidEth, address(WETH), 0, 0, abi.encode(0, address(this), weETHs_teller, true)
address(boringSolver),
liquidEth,
address(WETH),
0,
0,
abi.encode(0, address(this), weETHs_teller, true, false)
);

// Redeem Mint Solve teller mismatch revert.
Expand All @@ -716,7 +791,7 @@ contract BoringQueueTest is Test, MerkleTreeHelper {
address(WETH),
0,
0,
abi.encode(1, address(this), weETHs_teller, liquidEth_teller, WETH, true)
abi.encode(1, address(this), weETHs_teller, liquidEth_teller, WETH, true, false)
);

vm.expectRevert(
Expand All @@ -732,7 +807,7 @@ contract BoringQueueTest is Test, MerkleTreeHelper {
address(WETH),
0,
0,
abi.encode(1, address(this), liquidEth_teller, weETHs_teller, WETH, true)
abi.encode(1, address(this), liquidEth_teller, weETHs_teller, WETH, true, false)
);
vm.stopPrank();

Expand Down
2 changes: 1 addition & 1 deletion test/BoringQueueWithTracking.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ contract BoringQueueWithtrackingTest is Test, MerkleTreeHelper {
skip(3 days);

uint256 wETHDelta = WETH.balanceOf(address(this));
boringSolver.boringRedeemSolve(requests, liquidEth_teller);
boringSolver.boringRedeemSolve(requests, liquidEth_teller, false);
wETHDelta = WETH.balanceOf(address(this)) - wETHDelta;
uint256 endingShares = ERC20(liquidEth).balanceOf(testUser);

Expand Down
Loading