Renegade v2#575
Conversation
duncancmt
left a comment
There was a problem hiding this comment.
General style in this repo is that constant arguments go on the left of commutative operations in assembly. E.g. add(0x40, data) ⭕ -- add(data, 0x40) ❌
If this isn't already in AGENTS.md, it should be
| assembly ("memory-safe") { | ||
| recipient := mload(add(data, 0x40)) | ||
| } | ||
| require((recipient == address(0)) || (recipient == address(this)), "Renegade: bad recipient"); |
There was a problem hiding this comment.
What does address(0) indicate?
| // newBuyAmt = floor(newSellAmt / price), matching | ||
| // FixedPointLib.divIntegerByFixedPoint. | ||
| uint256 newBuyAmt = (newSellAmt << 63) / priceRepr; |
There was a problem hiding this comment.
If we need to handle overflow, left shift should be avoided. If we don't need to handle overflow / should become unsafeDiv
| require(newBuyAmt >= minInternalPartyAmountIn, "Renegade: newBuyAmt < minInternalPartyAmountIn"); | ||
| require(newBuyAmt <= maxInternalPartyAmountIn, "Renegade: newBuyAmt > maxInternalPartyAmountIn"); |
There was a problem hiding this comment.
Presumably Renegade checks this internally? If it doesn't, please replace this with the pattern:
if (!condition) {
revert MyCustomErrorSelector(...);
}
or, even better, throw the error in assembly. This is for contract size. If this isn't already documented as a stylistic dictum in AGENTS.md, please add it.
| // Snapshot the buyToken balance before the call so we can measure the | ||
| // actual amount transferred after. | ||
| uint256 buyTokenBalanceBefore = buyToken.fastBalanceOf(address(this)); |
There was a problem hiding this comment.
This is an antipattern for slippage checking https://kebabsec.xyz/posts/critical_vulnerability_in_uniswapx/
| // Check the balance delta. | ||
| buyAmt = buyToken.fastBalanceOf(address(this)) - buyTokenBalanceBefore; | ||
| if (buyAmt < minBuyAmt) revertTooMuchSlippage(buyToken, minBuyAmt, buyAmt); |
There was a problem hiding this comment.
Shouldn't this be handled by _checkSlippageAndTransfer? Also is custody optimization not possible with RenegadeV2?
| * 0x140 uint256 <offset> GasSponsorOptions: (address refundAddress, bool refundNativeEth, uint256 refundAmount, uint256 nonce, bytes signature) | ||
| * ) | ||
| */ | ||
| uint32 internal constant RENEGADE_SELECTOR = uint32( |
There was a problem hiding this comment.
If you define this constant as a literal, then you'll be able to access it in assembly. Even though the current expression is compile-time constant, because it isn't literal, it can't be accessed from Yul. Also should probably be private
🛡️ Immunefi PR ReviewsWe noticed that your project isn't set up for automatic code reviews. If you'd like this PR reviewed by the Immunefi team, you can request it manually using the link below: Once submitted, we'll take care of assigning a reviewer and follow up here. |
| } else { // if (action == uint32(ISettlerActions.PANCAKE_INFINITY.selector)) | ||
| } else { | ||
| // if (action == uint32(ISettlerActions.PANCAKE_INFINITY.selector)) | ||
| sellToPancakeInfinity(recipient, sellToken, bps, feeOnTransfer, hashMul, hashMod, fills, amountOutMin); | ||
| } | ||
| /* | ||
| } else if (action == uint32(ISettlerActions.EULERSWAP.selector)) { | ||
| (address recipient, IERC20 sellToken, uint256 bps, IEulerSwap pool, bool zeroForOne, uint256 amountOutMin) = | ||
| abi.decode(data, (address, IERC20, uint256, IEulerSwap, bool, uint256)); | ||
| /* | ||
| } else if (action == uint32(ISettlerActions.EULERSWAP.selector)) { | ||
| (address recipient, IERC20 sellToken, uint256 bps, IEulerSwap pool, bool zeroForOne, uint256 amountOutMin) = | ||
| abi.decode(data, (address, IERC20, uint256, IEulerSwap, bool, uint256)); | ||
|
|
||
| sellToEulerSwap(recipient, sellToken, bps, pool, zeroForOne, amountOutMin); | ||
| */ | ||
| sellToEulerSwap(recipient, sellToken, bps, pool, zeroForOne, amountOutMin); | ||
| */ |
There was a problem hiding this comment.
please fix bad formatting
| if ((recipient != address(0)) && (recipient != address(this))) revertInvalidRenegadeData(); | ||
| if (sellToken != internalPartyOutputToken) revertInvalidRenegadeData(); | ||
| if (priceRepr == 0) revertInvalidRenegadeData(); |
There was a problem hiding this comment.
Join these together with FastLogic.or into a single branch to save bytecode size.
| if (newBuyAmt < minInternalPartyAmountIn) revertInvalidRenegadeData(); | ||
| if (newBuyAmt > maxInternalPartyAmountIn) revertInvalidRenegadeData(); |
There was a problem hiding this comment.
combine these with the above checks as well, presuming that these checks are meaningful and not redundant, which I suspect they are
| revertTooMuchSlippage(_extractBuyToken(data, baseForQuote), minBuyAmount, buyAmount); | ||
| } | ||
| address target = _renegadeGasSponsorV2(); | ||
| uint256 buyTokenBalanceBefore = buyToken.fastBalanceOf(address(this)); |
There was a problem hiding this comment.
This is still the forbidden way of checking slippage. Please fix.
| let len := mload(data) | ||
| // temporarily clobber `data` size memory area | ||
| mstore(data, selector) | ||
| // Allowed selectors don't clash with any relevant function of restricted targets so we can skip checking `target` |
There was a problem hiding this comment.
Please restore this comment
| } | ||
|
|
||
| buyAmt = buyToken.fastBalanceOf(address(this)) - buyTokenBalanceBefore; | ||
| if (buyAmt < minBuyAmt) revertTooMuchSlippage(buyToken, minBuyAmt, buyAmt); |
| /// VIP actions should always start with `recipient` address and the `permit` from the taker | ||
| /// followed by all the other parameters to ensure compatibility with `executeWithPermit` entrypoint. | ||
| /// `minBuyAmount`/`amountOutMin` should always be the last parameter. | ||
|
|
There was a problem hiding this comment.
Please fix bad formatting
duncancmt
left a comment
There was a problem hiding this comment.
This is looking pretty good. I think I still don't all the way understand how we're expecting Renegade to interact with slippage checking and custody optimization. The previous iteration seemed like it was amenable to custody optimization (and consequently slippage checking), but the current iteration removes that functionality entirely. Is this because we don't know whether we'll be applying that optimization at the time we request the firm quote from Renegade and the recipient is cryptographically bound to the price?
Even if custody optimization isn't possible, it's still desirable to be able to slippage-check the Renegade leg. Per-leg slippage checking is an infrequent, but still-used idiom.
Lastly, if we're going to be destructuring/interpreting many of the fields of the arguments to sponsorExternalMatch, it would make sense for those fields to be arguments of the action rather than taking it as opaque bytes and implicitly extracting them. It's less error-prone and (sometimes) more gas efficient. If we're not reading any of the BoundedMatchResult fields, then that can still be opaque.
Feel free to DM me if you want to iterate faster.
| // FixedPointLib.divIntegerByFixedPoint. | ||
| uint256 newBuyAmt = (newSellAmt << 63).unsafeDiv(priceRepr); | ||
|
|
||
| if (newBuyAmt < minBuyAmt) revertTooMuchSlippage(buyToken, minBuyAmt, newBuyAmt); |
There was a problem hiding this comment.
Why was this removed? Does Renegade not support custody optimization?
| if (newBuyAmt < minInternalPartyAmountIn) revertInvalidRenegadeData(); | ||
| if (newBuyAmt > maxInternalPartyAmountIn) revertInvalidRenegadeData(); |
There was a problem hiding this comment.
just an idle thought here: should we clamp? if there's a maximum notional, if the swap encounters slippage, we should clamp to avoid a revert, no?
| // `data` is the args to `sponsorExternalMatch` minus the 4-byte selector; its payload begins at | ||
| // `data + 0x20`. The head is static (`BoundedMatchResult` is inlined), so we index fixed offsets: | ||
| // 0x20 externalPartyAmountIn, 0x40 recipient, 0x80 internalPartyOutputToken. | ||
| uint32 private constant RENEGADE_SELECTOR = 0x54ea46d4; |
There was a problem hiding this comment.
it doesn't necessarily need to be here, but somewhere there should be a more readable signature from which this selector was computed
| } | ||
|
|
||
| buyAmt = buyToken.fastBalanceOf(address(this)) - buyTokenBalanceBefore; | ||
| if (buyAmt < minBuyAmt) revertTooMuchSlippage(buyToken, minBuyAmt, buyAmt); |
Picked up from #567
Context from 567:
Renegade v2 is deployed to Base Mainnet and Arbitrum One. On both deployments, the function signature for settlement is the same: sponsorExternalMatch(uint256,address,(address,address,(uint256),uint256,uint256,uint256),(bool,uint8,bytes),(address,bool,uint256,uint256,bytes))
"The adapter contract exposes sellToRenegade() with the following signature. Unlike the v1 adapter contract, there is no bool baseForQuote parameter.