From d525f3344767750bfd773b41628313281ee43c26 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 10 Mar 2025 12:05:01 +0100 Subject: [PATCH 1/4] feat: add priliminary support for signed orders (off-chain auctions). TODO: expose interface and validate signature --- src/fillers/BaseFiller.sol | 6 +- src/fillers/coin/CoinFiller.sol | 2 +- src/fillers/coin/SignedCoinFiller.sol | 101 ++++++++++++++++++++ src/fillers/coin/types/SignedOutputType.sol | 53 ++++++++++ 4 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 src/fillers/coin/SignedCoinFiller.sol create mode 100644 src/fillers/coin/types/SignedOutputType.sol diff --git a/src/fillers/BaseFiller.sol b/src/fillers/BaseFiller.sol index 0a79c52b..44466187 100644 --- a/src/fillers/BaseFiller.sol +++ b/src/fillers/BaseFiller.sol @@ -35,6 +35,10 @@ abstract contract BaseFiller is IPayloadCreator { function _preDeliveryHook(address recipient, address token, uint256 outputAmount) internal virtual returns (uint256); + function _getOutputDescriptionHash(OutputDescription calldata output) internal virtual pure returns (bytes32 outputHash) { + return OutputEncodingLib.getOutputDescriptionHash(output); + } + /** * @notice Verifies & Fills an order. * If an order has already been filled given the output & fillDeadline, then this function @@ -58,7 +62,7 @@ abstract contract BaseFiller is IPayloadCreator { _IAmRemoteFiller(output.remoteFiller); // Get hash of output. - bytes32 outputHash = OutputEncodingLib.getOutputDescriptionHash(output); + bytes32 outputHash = _getOutputDescriptionHash(output); // Get the proof state of the fulfillment. bytes32 existingSolver = _filledOutputs[orderId][outputHash].solver; diff --git a/src/fillers/coin/CoinFiller.sol b/src/fillers/coin/CoinFiller.sol index f3912d1d..007336f7 100644 --- a/src/fillers/coin/CoinFiller.sol +++ b/src/fillers/coin/CoinFiller.sol @@ -31,7 +31,7 @@ contract CoinFiller is BaseFiller { */ function _getAmount( OutputDescription calldata output - ) internal view returns (uint256 amount) { + ) internal virtual view returns (uint256 amount) { uint256 fulfillmentLength = output.fulfillmentContext.length; if (fulfillmentLength == 0) return output.amount; bytes1 orderType = bytes1(output.fulfillmentContext); diff --git a/src/fillers/coin/SignedCoinFiller.sol b/src/fillers/coin/SignedCoinFiller.sol new file mode 100644 index 00000000..673757a8 --- /dev/null +++ b/src/fillers/coin/SignedCoinFiller.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import { EIP712 } from "solady/utils/EIP712.sol"; +import { SignatureCheckerLib } from "solady/utils/SignatureCheckerLib.sol"; + +import { CoinFillerWithFee } from "./CoinFillerWithFee.sol"; +import { OutputDescription, OutputEncodingLib } from "src/libs/OutputEncodingLib.sol"; +import { SignedOutputType } from "./types/SignedOutputType.sol"; + +/** + * @dev Solvers use Oracles to pay outputs. This allows us to record the payment. + * Tokens never touch this contract but goes directly from solver to user. + */ +contract SignedCoinFiller is CoinFillerWithFee, EIP712 { + error InvalidSigner(); + + constructor( + address owner + ) payable CoinFillerWithFee(owner) { } + + function _domainNameAndVersion() internal pure virtual override returns (string memory name, string memory version) { + name = "SignedCompactFiller"; + version = "Signed1"; + } + + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return _domainSeparator(); + } + + /** + * @notice Removed the first 32 bytes of fulfillmentContext, since that is expected to have been + * prepended by the auction server. + */ + function _getOutputDescriptionHash( + OutputDescription calldata outputDescription + ) internal override pure returns (bytes32 outputHash) { + bytes calldata remoteCall = outputDescription.remoteCall; + bytes calldata fulfillmentContext = outputDescription.fulfillmentContext; + // Check that the length of remoteCall & fulfillmentContext does not exceed type(uint16).max + if (remoteCall.length > type(uint16).max) revert OutputEncodingLib.RemoteCallOutOfRange(); + if (fulfillmentContext.length > type(uint16).max) revert OutputEncodingLib.FulfillmentContextCallOutOfRange(); + + return outputHash = keccak256(abi.encodePacked( + outputDescription.remoteOracle, + outputDescription.remoteFiller, + outputDescription.chainId, + outputDescription.token, + outputDescription.amount, + outputDescription.recipient, + uint16(remoteCall.length), // To protect against data collisions + remoteCall, + uint16(fulfillmentContext.length), // To protect against data collisions + fulfillmentContext[32:fulfillmentContext.length] + )); + } + + function _contextTrueAmount(bytes calldata fulfillmentContext) internal pure returns (uint256 amount) { + // amount = uint256(bytes32(output.fulfillmentContext[0:32]))); + assembly ("memory-safe") { + amount := calldataload(add(fulfillmentContext.offset, 0x00)) + } + } + + function _contextSigner(bytes calldata fulfillmentContext) internal pure returns (address signer) { + // signer = address(uint160(uint256(bytes32(output.fulfillmentContext[33:65]))); + assembly ("memory-safe") { + signer := calldataload(add(fulfillmentContext.offset, 0x21)) + } + } + + /** + * @notice Computes the amount of an order. Allows limit orders and dutch auctions. + * @dev Uses the fulfillmentContext of the output to determine order type. + * This contract only understand off-chain auction swaps. + * Structure: + * uint256(trueAmount) | bytes1(orderTypeIdentifier) | signer | ...... + * In the actual order, bytes1(orderTypeIdentifier) is the first byte but the order server pre-pends the trueAmount + */ + function _getAmount( + OutputDescription calldata output + ) internal override pure returns (uint256 amount) { + amount = _contextTrueAmount(output.fulfillmentContext); + // We don't care about the rest of fulfillmentContext. That is used for off-chain services. + } + + function _validateSolver( + OutputDescription calldata output, + bytes32 solver, + bytes calldata signature + ) view internal { + bytes calldata fulfillmentContext = output.fulfillmentContext; + uint256 amount = _contextTrueAmount(fulfillmentContext); + address signer = _contextSigner(fulfillmentContext); + + bytes32 digest = _hashTypedData(SignedOutputType.hashSignedOutput(output, solver, amount)); + + bool isValid = SignatureCheckerLib.isValidSignatureNowCalldata(signer, digest, signature); + if (!isValid) revert InvalidSigner(); + } +} diff --git a/src/fillers/coin/types/SignedOutputType.sol b/src/fillers/coin/types/SignedOutputType.sol new file mode 100644 index 00000000..612d0ffd --- /dev/null +++ b/src/fillers/coin/types/SignedOutputType.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import { OutputDescription } from "src/libs/OutputEncodingLib.sol"; + +/** + * @notice Signed struct + */ +struct SignedOutput { + bytes32 winner; + + bytes32 remoteOracle; + bytes32 remoteFiller; + uint256 chainId; + bytes32 token; + uint256 amount; + /** @dev Ammended amount to the original order. */ + uint256 trueAmount; + bytes32 recipient; + bytes remoteCall; + bytes fulfillmentContext; +} + +/** + * @notice Helper library for the Signed Output type. + * TYPE_PARTIAL: An incomplete type. Is missing a field. + * TYPE_STUB: Type has no subtypes. + * TYPE: Is complete including sub-types. + */ +library SignedOutputType { + bytes constant SIGNED_OUTPUT_TYPE_STUB = abi.encodePacked( + "OutputDescription(" "bytes32 winner," "bytes32 remoteOracle," "bytes32 remoteFiller," "uint256 chainId," "bytes32 token," "uint256 amount," "uint256 trueAmount," "bytes32 recipient," "bytes remoteCall," "bytes fulfillmentContext" ")" + ); + + bytes32 constant SIGNED_OUTPUT_TYPE_HASH = keccak256(SIGNED_OUTPUT_TYPE_STUB); + + function hashSignedOutput(OutputDescription calldata output, bytes32 winner, uint256 trueAmount) internal pure returns (bytes32) { + return keccak256(abi.encode( + SIGNED_OUTPUT_TYPE_HASH, + winner, + output.remoteOracle, + output.remoteFiller, + output.chainId, + output.token, + output.amount, + trueAmount, + output.recipient, + keccak256(output.remoteCall), + keccak256(output.fulfillmentContext) + ) + ); + } +} From a7db95b4fd9c0eefd5ba2dfe5c18e1694ef53493 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 10 Mar 2025 12:29:10 +0100 Subject: [PATCH 2/4] feat: external endpoint --- src/fillers/coin/SignedCoinFiller.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/fillers/coin/SignedCoinFiller.sol b/src/fillers/coin/SignedCoinFiller.sol index 673757a8..06aec29d 100644 --- a/src/fillers/coin/SignedCoinFiller.sol +++ b/src/fillers/coin/SignedCoinFiller.sol @@ -98,4 +98,9 @@ contract SignedCoinFiller is CoinFillerWithFee, EIP712 { bool isValid = SignatureCheckerLib.isValidSignatureNowCalldata(signer, digest, signature); if (!isValid) revert InvalidSigner(); } + + function fill(bytes32 orderId, OutputDescription calldata output, bytes32 proposedSolver, bytes calldata signature) external returns (bytes32) { + _validateSolver(output, proposedSolver, signature); + return _fill(orderId, output, proposedSolver); + } } From 9b4426038eb51618969e4623de52652ca342d3dd Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 18 Mar 2025 15:10:57 +0000 Subject: [PATCH 3/4] feat: add offset for order type identifier and use the amount as minout --- src/fillers/coin/SignedCoinFiller.sol | 13 ++++++++----- src/fillers/coin/types/SignedOutputType.sol | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/fillers/coin/SignedCoinFiller.sol b/src/fillers/coin/SignedCoinFiller.sol index 06aec29d..26d88fc1 100644 --- a/src/fillers/coin/SignedCoinFiller.sol +++ b/src/fillers/coin/SignedCoinFiller.sol @@ -56,16 +56,16 @@ contract SignedCoinFiller is CoinFillerWithFee, EIP712 { } function _contextTrueAmount(bytes calldata fulfillmentContext) internal pure returns (uint256 amount) { - // amount = uint256(bytes32(output.fulfillmentContext[0:32]))); + // amount = uint256(bytes32(output.fulfillmentContext[1:33]))); assembly ("memory-safe") { - amount := calldataload(add(fulfillmentContext.offset, 0x00)) + amount := calldataload(add(fulfillmentContext.offset, 0x01)) } } function _contextSigner(bytes calldata fulfillmentContext) internal pure returns (address signer) { - // signer = address(uint160(uint256(bytes32(output.fulfillmentContext[33:65]))); + // signer = address(uint160(uint256(bytes32(output.fulfillmentContext[34:66]))); assembly ("memory-safe") { - signer := calldataload(add(fulfillmentContext.offset, 0x21)) + signer := calldataload(add(fulfillmentContext.offset, 0x22)) } } @@ -74,7 +74,7 @@ contract SignedCoinFiller is CoinFillerWithFee, EIP712 { * @dev Uses the fulfillmentContext of the output to determine order type. * This contract only understand off-chain auction swaps. * Structure: - * uint256(trueAmount) | bytes1(orderTypeIdentifier) | signer | ...... + * 0x80 | uint256(trueAmount) | bytes1(orderTypeIdentifier) | signer | ...... * In the actual order, bytes1(orderTypeIdentifier) is the first byte but the order server pre-pends the trueAmount */ function _getAmount( @@ -91,6 +91,9 @@ contract SignedCoinFiller is CoinFillerWithFee, EIP712 { ) view internal { bytes calldata fulfillmentContext = output.fulfillmentContext; uint256 amount = _contextTrueAmount(fulfillmentContext); + // The output amount shall describe a minimum output such + // that a fradulent signer cannot cheat the user. + amount = amount >= output.amount ? amount : output.amount; address signer = _contextSigner(fulfillmentContext); bytes32 digest = _hashTypedData(SignedOutputType.hashSignedOutput(output, solver, amount)); diff --git a/src/fillers/coin/types/SignedOutputType.sol b/src/fillers/coin/types/SignedOutputType.sol index 612d0ffd..71cdbd5c 100644 --- a/src/fillers/coin/types/SignedOutputType.sol +++ b/src/fillers/coin/types/SignedOutputType.sol @@ -14,7 +14,7 @@ struct SignedOutput { uint256 chainId; bytes32 token; uint256 amount; - /** @dev Ammended amount to the original order. */ + /** @dev Amended amount to the original order. */ uint256 trueAmount; bytes32 recipient; bytes remoteCall; From e4da208eff0e73bc76c0d0dc81ae0367ae610b94 Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 18 Mar 2025 15:12:16 +0000 Subject: [PATCH 4/4] fix: typo of fraudulent --- src/fillers/coin/SignedCoinFiller.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fillers/coin/SignedCoinFiller.sol b/src/fillers/coin/SignedCoinFiller.sol index 26d88fc1..4076263b 100644 --- a/src/fillers/coin/SignedCoinFiller.sol +++ b/src/fillers/coin/SignedCoinFiller.sol @@ -92,7 +92,7 @@ contract SignedCoinFiller is CoinFillerWithFee, EIP712 { bytes calldata fulfillmentContext = output.fulfillmentContext; uint256 amount = _contextTrueAmount(fulfillmentContext); // The output amount shall describe a minimum output such - // that a fradulent signer cannot cheat the user. + // that a fraudulent signer cannot cheat the user. amount = amount >= output.amount ? amount : output.amount; address signer = _contextSigner(fulfillmentContext);