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

feat: FlashMintDex #181

Merged
merged 61 commits into from
Aug 22, 2024
Merged

feat: FlashMintDex #181

merged 61 commits into from
Aug 22, 2024

Conversation

edkim
Copy link
Contributor

@edkim edkim commented Aug 2, 2024

Contract that allows FlashMint and FlashRedeem using any DEX supported by the DEXAdapterV2 library.
Supports SetTokens on Index Protocol or Set Protocol to avoid needing two deployments.

Frontends can use the getIssueExactSet function to return the amount of ETH/ERC20 required to Flash Mint, using on-chain DEX prices. Likewise getRedeemExactSet returns how much ETH/ERC20 the user will receive after Flash Redeeming.

To run tests:

  1. yarn chain:fork:ethereum
  2. INTEGRATIONTEST=true VERBOSE=true npx hardhat test ./test/integration/ethereum/flashMintDex.spec.ts --network localhost

@edkim edkim marked this pull request as ready for review August 13, 2024 19:08
expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(paymentInfo.limitAmt));
});

it("Can issue set token from USDC", async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@janndriessen see this test for how to call from the SDK.

Copy link
Collaborator

@ckoopmann ckoopmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work.
Mostly looks good to me.
Only major points I made are regarding the necessity of calling getWethReceived/Costs in the actual issuance process, that I think we should review

Rest of the comments are mostly just me thinking aloud or minor potential adjustments (like a token whitelist and avoid sending unnecessary calldata)

) {
const dexAdapter = await this.deployDEXAdapterV2();

const linkId = convertLibraryNameToLinkId(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why we will need to redeploy the DexAdapterV2 library ? (i.e. did any of the router addresses / configuration change )
If not we could reuse the existing library address that is already deployed, which you might also want to in the tests then.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a40e46a


modifier isValidModuleAndSet(address _issuanceModule, address _setToken) {
require(
setController.isModule(_issuanceModule) && setController.isSet(_setToken) ||
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might also want to check that the given issuance module is actually associated to that set token.
Otherwise if a user specifies the basic issuance module for a dim set token, it might result in weird behaviour ? (with regards to getting the redeem components further down for example)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also not sure if we really want to have all legacy set tokens (including those not managed by us) be enabled by default.

Maybe we keep a whitelist instead ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what user case is this input sanitization guarding against?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One scenario:
A user using this to issue some weird set token of their own creation, which might not actually be compatible with our assumptions and then blaming us for something going wrong.

* @param _setToken Set token to issue
* @param _amountSetToken Amount of set token to issue
*/
function getRequiredIssuanceComponents(address _issuanceModule, bool _isDebtIssuance, ISetToken _setToken, uint256 _amountSetToken) public view returns(address[] memory components, uint256[] memory positions) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not super important, but I was wondering what happens if a user tries to issue a leverage token via this contract. I guess the debt tokens returned will be stranded in the flash mint contract ?
In that case maybe we want to have an allow list of tokens that can be issued / redeemed with this contract ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up enforcing no external positions in dc7a943.

I preferred that to having a privileged user maintain a whitelist, but it does add gas on every issue/redeem.

require(ethUsedForIssuance <= msg.value, "FlashMint: OVERSPENT ETH");

uint256 ethReturned = msg.value.sub(ethUsedForIssuance);
if (ethReturned > 0) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there might be scenarios where ethReturned is 0.
Theoretically I can call this from a smart contract which then should be able to exactly calculate the eth to be spent beforehand in the same transaction.

uint256 leftoverWeth = wethReceived.sub(wethSpent);
uint256 paymentTokenReturned = 0;

if (leftoverWeth > 0) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above I think there are scenarios where the caller might be able to exactly predict the amount spent, so I'd keep this check.

contracts/exchangeIssuance/FlashMintDex.sol Show resolved Hide resolved
uint256 leftoverWeth = wethReceived.sub(wethSpent);
uint256 paymentTokenReturned = 0;

if (leftoverWeth > 0) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might think about letting the caller specify a minimum amount of leftoverWeth at which he wants to swap back though.
If the amount is 1 wei, it is definetely not worth the gas fees.

Alternatively maybe the caller could signal opting out of this by sending an empty swapDataWetToToken.

_redeem(_redeemParams.setToken, _redeemParams.amountSetToken, _redeemParams.issuanceModule);

uint256 wethReceived = _sellComponentsForWeth(_redeemParams);
outputTokenReceived = _swapWethForPaymentToken(wethReceived, _paymentInfo.token, _paymentInfo.swapDataWethToToken);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that in this instance we never use paymentInfo.swapDataTokenToWeth but still force the caller to send this.

You might want to create different versions of the PaymentInfo struct so that we don't force unnecessary calldata with the associated gas costs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I went with simplicity here rather than trying to optimize for the case that gas costs more than the value of token returned.

I'll see if I can split out that unused var (I was hitting stack too deep at one point).

address[] memory components,
uint256[] memory componentUnits,
uint256[] memory wethCosts
) = _getWethCostsPerComponent(_issueParams);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to calculate the weth costs here ?
We could just check wether the totalWethSpent is less that what the user provided no ?
Should save us some gas. Also there might be scenarios where the swap paths of the components overlap resulting in changing prices / weth costs between this call and the actual swaps.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, we shouldn't have to worry about checking exact amounts here. And I didn't think about the overlapping swaps!

Copy link
Contributor Author

@edkim edkim Aug 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 8963206 and refactored in 9b1708b

address[] memory components,
uint256[] memory componentUnits,
uint256[] memory wethReceived
) = _getWethReceivedPerComponent(_redeemParams);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question here. Why do we need to calculate wethReceived beforehand ?
We can just verify totalWethBought, no ?

fees: [500],
path: [addresses.tokens.weth, addresses.tokens.USDC],
pool: ADDRESS_ZERO,
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this swap data would have to be constructed for any ERC-20 input/output token. And I assume it would be best to find the best path dynamically or could it be hard-coded to paths [inputToken, WETH] or [WETH, outputToken]? @edkim

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, we'd need these paths defined for all input/output tokens we let users choose. I was imagining these would be hard-coded. The best priced path can change, but not often.

totalEthNeeded += wethCosts[i];
}
return dexAdapter.getAmountIn(_swapDataInputTokenToWeth, totalEthNeeded);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These getter functions are nice 🌟

import { DEXAdapterV2 } from "./DEXAdapterV2.sol";

/**
* @title FlashMintDex
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add an @author and @notice here, highlighting the SDK/API benefits in the notice

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 562f2d6

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good, key note imo is that no 3rd party quote provider is necessary (0x)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a mention of this to the notice.

Copy link
Contributor

@pblivin0x pblivin0x left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good. couple comments on adding documentation and making sure any reverts in the source are hit by sensible test cases

@edkim
Copy link
Contributor Author

edkim commented Aug 19, 2024

Almost ready for 2nd review. Last thing is to let the user specify a minimum threshold for leftover dust to be swapped and returned.

Copy link
Collaborator

@ckoopmann ckoopmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work. LGTM.
In particular the following changes since my last review look good:

  • Minimum weth threshold for swapping back left over weth
  • Avoid calculating weth costs for each component
  • External position checks

}
for (uint256 i = 0; i < components.length; i++) {
require(_issueParams.setToken.getExternalPositionModules(components[i]).length == 0, "FlashMint: EXTERNAL POSITION MODULES NOT SUPPORTED");
uint256 wethSold = dexAdapter.swapTokensForExactTokens(componentUnits[i], type(uint256).max, _issueParams.componentSwapData[i]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically here instead of max_uint256 you could specify the remaining weth available to be spent. That way you would get an earlier revert in case it overspends.

Won't make much difference in practice though so feel free to disregard.

@edkim edkim merged commit 3847aab into master Aug 22, 2024
5 checks passed
@edkim edkim deleted the ek/flashmint-dex branch August 22, 2024 17:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants