Skip to content

Commit

Permalink
fix(contracts-rfq): add forwardTo to ZapData for Zap Actions that d…
Browse files Browse the repository at this point in the history
…on't forward assets to the user (#3451)

* feat: scaffold finalToken, forwardTo in ZapDataV1

* feat: add finalToken, forwardTo

* feat: scaffold TokenZap.encodeZapData

* test: update existing tests

* test: forwardTo scenarios

* feat: token zeor address checks

* feat:scaffold token forwarding

* test: more revert cases

* feat: final token forwarding

* test: forwardTo behaviour in SIP

* feat: support optional `forwardTo` in SIP

* deploy: redeploy SIP, TokenZap
  • Loading branch information
ChiTimesChi authored Dec 10, 2024
1 parent 2e6bce6 commit e28e56e
Show file tree
Hide file tree
Showing 16 changed files with 1,179 additions and 254 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ interface ISynapseIntentPreviewer {
/// @dev Will not revert if the intent cannot be completed, returns empty values instead.
/// @dev Returns (amountIn, []) if the intent is a no-op (tokenIn == tokenOut).
/// @param swapQuoter Peripheral contract to use for swap quoting
/// @param forwardTo The address to which the proceeds of the intent should be forwarded to.
/// Note: if no forwarding is required (or done within the intent), use address(0).
/// @param tokenIn Initial token for the intent
/// @param tokenOut Final token for the intent
/// @param amountIn Initial amount of tokens to use for the intent
Expand All @@ -16,6 +18,7 @@ interface ISynapseIntentPreviewer {
/// Empty if the intent cannot be completed, or if intent is a no-op (tokenIn == tokenOut).
function previewIntent(
address swapQuoter,
address forwardTo,
address tokenIn,
address tokenOut,
uint256 amountIn
Expand Down
40 changes: 35 additions & 5 deletions packages/contracts-rfq/contracts/libs/ZapDataV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ library ZapDataV1 {
// Offsets of the fields in the packed ZapData struct
// uint16 version [000 .. 002)
// uint16 amountPosition [002 .. 004)
// address target [004 .. 024)
// bytes payload [024 .. ***)
// address finalToken [004 .. 024)
// address forwardTo [024 .. 044)
// address target [044 .. 064)
// bytes payload [064 .. ***)

// forgefmt: disable-start
uint256 private constant OFFSET_AMOUNT_POSITION = 2;
uint256 private constant OFFSET_TARGET = 4;
uint256 private constant OFFSET_PAYLOAD = 24;
uint256 private constant OFFSET_FINAL_TOKEN = 4;
uint256 private constant OFFSET_FORWARD_TO = 24;
uint256 private constant OFFSET_TARGET = 44;
uint256 private constant OFFSET_PAYLOAD = 64;
// forgefmt: disable-end

error ZapDataV1__InvalidEncoding();
Expand All @@ -44,13 +48,23 @@ library ZapDataV1 {
/// This will usually be `4 + 32 * n`, where `n` is the position of the token amount in
/// the list of parameters of the target function (starting from 0).
/// Or `AMOUNT_NOT_PRESENT` if the token amount is not encoded within `payload_`.
/// @param finalToken_ The token produced as a result of the Zap action (ERC20 or native gas token).
/// A zero address value signals that the Zap action doesn't result in any asset per se,
/// like bridging or depositing into a vault without an LP token.
/// Note: this parameter must be set to a non-zero value if the `forwardTo_` parameter is
/// set to a non-zero value.
/// @param forwardTo_ The address to which `finalToken` should be forwarded. This parameter is required only
/// if the Zap action does not automatically transfer the token to the intended recipient.
/// Otherwise, it must be set to address(0).
/// @param target_ Address of the target contract.
/// @param payload_ ABI-encoded calldata to be used for the `target_` contract call.
/// If the target function has the token amount as an argument, any placeholder amount value
/// can be used for the original ABI encoding of `payload_`. The placeholder amount will
/// be replaced with the actual amount, when the Zap Data is decoded.
function encodeV1(
uint16 amountPosition_,
address finalToken_,
address forwardTo_,
address target_,
bytes memory payload_
)
Expand All @@ -63,7 +77,7 @@ library ZapDataV1 {
if (amountPosition_ != AMOUNT_NOT_PRESENT && (uint256(amountPosition_) + 32 > payload_.length)) {
revert ZapDataV1__InvalidEncoding();
}
return abi.encodePacked(VERSION, amountPosition_, target_, payload_);
return abi.encodePacked(VERSION, amountPosition_, finalToken_, forwardTo_, target_, payload_);
}

/// @notice Extracts the version from the encoded Zap Data.
Expand All @@ -74,6 +88,22 @@ library ZapDataV1 {
}
}

/// @notice Extracts the finalToken address from the encoded Zap Data.
function finalToken(bytes calldata encodedZapData) internal pure returns (address finalToken_) {
// Load 32 bytes from the offset and shift it 96 bits to the right to get the highest 160 bits.
assembly {
finalToken_ := shr(96, calldataload(add(encodedZapData.offset, OFFSET_FINAL_TOKEN)))
}
}

Check warning

Code scanning / Slither

Assembly usage Warning


/// @notice Extracts the forwardTo address from the encoded Zap Data.
function forwardTo(bytes calldata encodedZapData) internal pure returns (address forwardTo_) {
// Load 32 bytes from the offset and shift it 96 bits to the right to get the highest 160 bits.
assembly {
forwardTo_ := shr(96, calldataload(add(encodedZapData.offset, OFFSET_FORWARD_TO)))
}
}

Check warning

Code scanning / Slither

Assembly usage Warning


/// @notice Extracts the target address from the encoded Zap Data.
function target(bytes calldata encodedZapData) internal pure returns (address target_) {
// Load 32 bytes from the offset and shift it 96 bits to the right to get the highest 160 bits.
Expand Down
70 changes: 49 additions & 21 deletions packages/contracts-rfq/contracts/router/SynapseIntentPreviewer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer {
/// @dev Amount value that signals that the Zap step should be performed using the full ZapRecipient balance.
uint256 internal constant FULL_BALANCE = type(uint256).max;

error SIP__NoOpForwardNotSupported();
error SIP__PoolTokenMismatch();
error SIP__PoolZeroAddress();
error SIP__RawParamsEmpty();
error SIP__TokenNotNative();

/// @inheritdoc ISynapseIntentPreviewer
// solhint-disable-next-line code-complexity
function previewIntent(
address swapQuoter,
address forwardTo,
address tokenIn,
address tokenOut,
uint256 amountIn
Expand All @@ -39,6 +42,7 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer {
{
// First, check if the intent is a no-op.
if (tokenIn == tokenOut) {
if (forwardTo != address(0)) revert SIP__NoOpForwardNotSupported();
return (amountIn, new ISynapseIntentRouter.StepParams[](0));
}

Expand All @@ -59,13 +63,13 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer {

// Create the steps for the intent based on the action type.
if (params.action == Action.Swap) {
steps = _createSwapSteps(tokenIn, tokenOut, amountIn, params);
steps = _createSwapSteps(tokenIn, tokenOut, amountIn, params, forwardTo);
} else if (params.action == Action.AddLiquidity) {
steps = _createAddLiquiditySteps(tokenIn, tokenOut, params);
steps = _createAddLiquiditySteps(tokenIn, tokenOut, params, forwardTo);
} else if (params.action == Action.RemoveLiquidity) {
steps = _createRemoveLiquiditySteps(tokenIn, tokenOut, params);
steps = _createRemoveLiquiditySteps(tokenIn, tokenOut, params, forwardTo);
} else {
steps = _createHandleHativeSteps(tokenIn, tokenOut, amountIn);
steps = _createHandleHativeSteps(tokenIn, tokenOut, amountIn, forwardTo);
}
}

Expand All @@ -74,7 +78,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer {
address tokenIn,
address tokenOut,
uint256 amountIn,
DefaultParams memory params
DefaultParams memory params,
address forwardTo
)
internal
view
Expand All @@ -89,10 +94,10 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer {
address wrappedNative = IDefaultPool(pool).getToken(params.tokenIndexFrom);
// Sanity check tokenOut vs tokenIndexTo.
if (IDefaultPool(pool).getToken(params.tokenIndexTo) != tokenOut) revert SIP__PoolTokenMismatch();
// Native => WrappedNative + WrappedNative => TokenOut.
// Native => WrappedNative + WrappedNative => TokenOut. Forwarding is done in the second step.
return _toStepsArray(
_createWrapNativeStep({wrappedNative: wrappedNative, amountIn: amountIn}),
_createSwapStep({tokenIn: wrappedNative, params: params})
_createWrapNativeStep({wrappedNative: wrappedNative, msgValue: amountIn, forwardTo: address(0)}),
_createSwapStep({tokenIn: wrappedNative, tokenOut: tokenOut, params: params, forwardTo: forwardTo})
);
}

Expand All @@ -103,25 +108,28 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer {
if (tokenOut == NATIVE_GAS_TOKEN) {
// Get the address of the wrapped native token.
address wrappedNative = IDefaultPool(pool).getToken(params.tokenIndexTo);
// TokenIn => WrappedNative + WrappedNative => Native.
// TokenIn => WrappedNative + WrappedNative => Native. Forwarding is done in the second step.
return _toStepsArray(
_createSwapStep({tokenIn: tokenIn, params: params}),
_createUnwrapNativeStep({wrappedNative: wrappedNative})
_createSwapStep({tokenIn: tokenIn, tokenOut: wrappedNative, params: params, forwardTo: address(0)}),
_createUnwrapNativeStep({wrappedNative: wrappedNative, forwardTo: forwardTo})
);
}

// Sanity check tokenOut vs tokenIndexTo.
if (IDefaultPool(pool).getToken(params.tokenIndexTo) != tokenOut) revert SIP__PoolTokenMismatch();

// TokenIn => TokenOut.
return _toStepsArray(_createSwapStep({tokenIn: tokenIn, params: params}));
ISynapseIntentRouter.StepParams memory step =
_createSwapStep({tokenIn: tokenIn, tokenOut: tokenOut, params: params, forwardTo: forwardTo});
return _toStepsArray(step);
}

/// @notice Helper function to create steps for adding liquidity.
function _createAddLiquiditySteps(
address tokenIn,
address tokenOut,
DefaultParams memory params
DefaultParams memory params,
address forwardTo
)
internal
view
Expand Down Expand Up @@ -152,6 +160,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer {
msgValue: 0,
zapData: ZapDataV1.encodeV1({
target_: pool,
finalToken_: tokenOut,
forwardTo_: forwardTo,
// addLiquidity(amounts, minToMint, deadline)
payload_: abi.encodeCall(IDefaultExtendedPool.addLiquidity, (amounts, 0, type(uint256).max)),
// amountIn is encoded within `amounts` at `TOKEN_IN_INDEX`, `amounts` is encoded after
Expand All @@ -166,7 +176,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer {
function _createRemoveLiquiditySteps(
address tokenIn,
address tokenOut,
DefaultParams memory params
DefaultParams memory params,
address forwardTo
)
internal
view
Expand All @@ -185,6 +196,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer {
msgValue: 0,
zapData: ZapDataV1.encodeV1({
target_: pool,
finalToken_: tokenOut,
forwardTo_: forwardTo,
// removeLiquidityOneToken(tokenAmount, tokenIndex, minAmount, deadline)
payload_: abi.encodeCall(
IDefaultExtendedPool.removeLiquidityOneToken, (0, params.tokenIndexTo, 0, type(uint256).max)
Expand All @@ -205,26 +218,31 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer {
function _createHandleHativeSteps(
address tokenIn,
address tokenOut,
uint256 amountIn
uint256 amountIn,
address forwardTo
)
internal
pure
returns (ISynapseIntentRouter.StepParams[] memory steps)
{
if (tokenIn == NATIVE_GAS_TOKEN) {
// tokenOut is Wrapped Native
return _toStepsArray(_createWrapNativeStep({wrappedNative: tokenOut, amountIn: amountIn}));
return _toStepsArray(
_createWrapNativeStep({wrappedNative: tokenOut, msgValue: amountIn, forwardTo: forwardTo})
);
}
// Sanity check tokenOut
if (tokenOut != NATIVE_GAS_TOKEN) revert SIP__TokenNotNative();
// tokenIn is Wrapped Native
return _toStepsArray(_createUnwrapNativeStep({wrappedNative: tokenIn}));
return _toStepsArray(_createUnwrapNativeStep({wrappedNative: tokenIn, forwardTo: forwardTo}));
}

/// @notice Helper function to create a single step for a swap.
function _createSwapStep(
address tokenIn,
DefaultParams memory params
address tokenOut,
DefaultParams memory params,
address forwardTo
)
internal
pure
Expand All @@ -236,6 +254,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer {
msgValue: 0,
zapData: ZapDataV1.encodeV1({
target_: params.pool,
finalToken_: tokenOut,
forwardTo_: forwardTo,
// swap(tokenIndexFrom, tokenIndexTo, dx, minDy, deadline)
payload_: abi.encodeCall(
IDefaultPool.swap, (params.tokenIndexFrom, params.tokenIndexTo, 0, 0, type(uint256).max)
Expand All @@ -249,7 +269,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer {
/// @notice Helper function to create a single step for wrapping native gas tokens.
function _createWrapNativeStep(
address wrappedNative,
uint256 amountIn
uint256 msgValue,
address forwardTo
)
internal
pure
Expand All @@ -258,9 +279,11 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer {
return ISynapseIntentRouter.StepParams({
token: NATIVE_GAS_TOKEN,
amount: FULL_BALANCE,
msgValue: amountIn,
msgValue: msgValue,
zapData: ZapDataV1.encodeV1({
target_: wrappedNative,
finalToken_: wrappedNative,
forwardTo_: forwardTo,
// deposit()
payload_: abi.encodeCall(IWETH9.deposit, ()),
// amountIn is not encoded
Expand All @@ -270,7 +293,10 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer {
}

/// @notice Helper function to create a single step for unwrapping native gas tokens.
function _createUnwrapNativeStep(address wrappedNative)
function _createUnwrapNativeStep(
address wrappedNative,
address forwardTo
)
internal
pure
returns (ISynapseIntentRouter.StepParams memory)
Expand All @@ -281,6 +307,8 @@ contract SynapseIntentPreviewer is ISynapseIntentPreviewer {
msgValue: 0,
zapData: ZapDataV1.encodeV1({
target_: wrappedNative,
finalToken_: NATIVE_GAS_TOKEN,
forwardTo_: forwardTo,
// withdraw(amount)
payload_: abi.encodeCall(IWETH9.withdraw, (0)),
// amountIn encoded as the first parameter
Expand Down
Loading

0 comments on commit e28e56e

Please sign in to comment.