packages/contracts-rfq/contracts/libs/ZapDataV1.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
// solhint-disable no-inline-assembly
library ZapDataV1 {
/// @notice Version of the Zap Data struct.
uint16 internal constant VERSION = 1;
/// @notice Value that indicates the amount is not present in the target function's payload.
uint16 internal constant AMOUNT_NOT_PRESENT = 0xFFFF;
// Offsets of the fields in the packed ZapData struct
// uint16 version [000 .. 002)
// uint16 amountPosition [002 .. 004)
// 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_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();
error ZapDataV1__TargetZeroAddress();
error ZapDataV1__UnsupportedVersion(uint16 version);
/// @notice Validates that encodedZapData is a tightly packed encoded payload for ZapData struct.
/// @dev Checks that all the required fields are present and the version is correct.
function validateV1(bytes calldata encodedZapData) internal pure {
// Check the minimum length: must at least include all static fields.
if (encodedZapData.length < OFFSET_PAYLOAD) revert ZapDataV1__InvalidEncoding();
// Once we validated the length, we can be sure that the version field is present.
uint16 version_ = version(encodedZapData);
if (version_ != VERSION) revert ZapDataV1__UnsupportedVersion(version_);
}
/// @notice Encodes the ZapData struct by tightly packing the fields.
/// Note: we don't know the exact amount of tokens that will be used for the Zap at the time of encoding,
/// so we provide the reference index where the token amount is encoded within `payload_`. This allows us to
/// hot-swap the token amount in the payload, when the Zap is performed.
/// @dev `abi.decode` will not work as a result of the tightly packed fields. Use `decodeZapData` instead.
/// @param amountPosition_ Position (start index) where the token amount is encoded within `payload_`.
/// 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_
)
internal
pure
returns (bytes memory encodedZapData)
{
if (target_ == address(0)) revert ZapDataV1__TargetZeroAddress();
// Amount is encoded in [amountPosition_ .. amountPosition_ + 32), which should be within the payload.
if (amountPosition_ != AMOUNT_NOT_PRESENT && (uint256(amountPosition_) + 32 > payload_.length)) {
revert ZapDataV1__InvalidEncoding();
}
return abi.encodePacked(VERSION, amountPosition_, finalToken_, forwardTo_, target_, payload_);
}
/// @notice Extracts the version from the encoded Zap Data.
function version(bytes calldata encodedZapData) internal pure returns (uint16 version_) {
// Load 32 bytes from the start and shift it 240 bits to the right to get the highest 16 bits.
assembly {
version_ := shr(240, calldataload(encodedZapData.offset))
}
}
/// @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)))
}
}
/// @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)))
}
}
/// @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.
assembly {
target_ := shr(96, calldataload(add(encodedZapData.offset, OFFSET_TARGET)))
}
}
/// @notice Extracts the payload from the encoded Zap Data. Replaces the token amount with the provided value,
/// if it was present in the original data (if amountPosition is not AMOUNT_NOT_PRESENT).
/// @dev This payload will be used as a calldata for the target contract.
function payload(bytes calldata encodedZapData, uint256 amount) internal pure returns (bytes memory) {
// The original payload is located at encodedZapData[OFFSET_PAYLOAD:].
uint16 amountPosition = _amountPosition(encodedZapData);
// If the amount was not present in the original payload, return the payload as is.
if (amountPosition == AMOUNT_NOT_PRESENT) {
return encodedZapData[OFFSET_PAYLOAD:];
}
// Calculate the start and end indexes of the amount in ZapData from its position within the payload.
// Note: we use inclusive start and exclusive end indexes for easier slicing of the ZapData.
uint256 amountStartIndexIncl = OFFSET_PAYLOAD + amountPosition;
uint256 amountEndIndexExcl = amountStartIndexIncl + 32;
// Check that the amount is within the ZapData.
if (amountEndIndexExcl > encodedZapData.length) revert ZapDataV1__InvalidEncoding();
// Otherwise we need to replace the amount in the payload with the provided value.
return abi.encodePacked(
// Copy the original payload up to the amount
encodedZapData[OFFSET_PAYLOAD:amountStartIndexIncl],
// Replace the originally encoded amount with the provided value
amount,
// Copy the rest of the payload after the amount
encodedZapData[amountEndIndexExcl:]
);
}
/// @notice Extracts the amount position from the encoded Zap Data.
function _amountPosition(bytes calldata encodedZapData) private pure returns (uint16 amountPosition) {
// Load 32 bytes from the offset and shift it 240 bits to the right to get the highest 16 bits.
assembly {
amountPosition := shr(240, calldataload(add(encodedZapData.offset, OFFSET_AMOUNT_POSITION)))
}
}
}