-
Notifications
You must be signed in to change notification settings - Fork 48
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
feat: FlashMintDex #181
Conversation
expect(inputTokenBalanceAfter).to.gt(inputTokenBalanceBefore.sub(paymentInfo.limitAmt)); | ||
}); | ||
|
||
it("Can issue set token from USDC", async () => { |
There was a problem hiding this comment.
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.
There was a problem hiding this 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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) || |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
uint256 leftoverWeth = wethReceived.sub(wethSpent); | ||
uint256 paymentTokenReturned = 0; | ||
|
||
if (leftoverWeth > 0) { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
address[] memory components, | ||
uint256[] memory componentUnits, | ||
uint256[] memory wethReceived | ||
) = _getWethReceivedPerComponent(_redeemParams); |
There was a problem hiding this comment.
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, | ||
}; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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); | ||
} |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added in 562f2d6
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this 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
Almost ready for 2nd review. Last thing is to let the user specify a minimum threshold for leftover dust to be swapped and returned. |
There was a problem hiding this 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]); |
There was a problem hiding this comment.
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.
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. LikewisegetRedeemExactSet
returns how much ETH/ERC20 the user will receive after Flash Redeeming.To run tests:
yarn chain:fork:ethereum
INTEGRATIONTEST=true VERBOSE=true npx hardhat test ./test/integration/ethereum/flashMintDex.spec.ts --network localhost