diff --git a/foundry.toml b/foundry.toml index ee0a33cf..0d8f8217 100644 --- a/foundry.toml +++ b/foundry.toml @@ -47,3 +47,4 @@ g-uni-v1-core = { version = "0.9.9", git = "git@github.com:Axis-Fi/g-uni-v1-core solmate = { version = "6.7.0", git = "git@github.com:transmissions11/solmate.git", rev = "c892309933b25c03d32b1b0d674df7ae292ba925" } clones-with-immutable-args = { version = "1.1.1", git = "git@github.com:wighawag/clones-with-immutable-args.git", rev = "f5ca191afea933d50a36d101009b5644dc28bc99" } solady = { version = "0.0.124" } +splits-waterfall = { version = "1.0.0", git = "git@github.com:0xSplits/splits-waterfall.git", rev = "1491f8b9454f22cf8a7bf5b384f5cdb75c9105b1" } diff --git a/remappings.txt b/remappings.txt index 5a87e55f..c7b2590b 100644 --- a/remappings.txt +++ b/remappings.txt @@ -14,4 +14,5 @@ @uniswap/v3-core/contracts=dependencies/@uniswap-v3-core-1.0.1-solc-0.8-simulate/contracts @openzeppelin/contracts=dependencies/@openzeppelin-contracts-4.9.2 @openzeppelin/contracts-upgradeable=dependencies/@openzeppelin-contracts-upgradeable-4.9.2 -@solady-0.0.124=dependencies/solady-0.0.124/src \ No newline at end of file +@solady-0.0.124=dependencies/solady-0.0.124/src +@splits-waterfall-1.0.0=dependencies/splits-waterfall-1.0.0/src \ No newline at end of file diff --git a/script/install.sh b/script/install.sh index 1ee7512e..2eede843 100755 --- a/script/install.sh +++ b/script/install.sh @@ -14,7 +14,14 @@ echo "*** Installing forge dependencies" forge install echo " Done" +echo "" +echo "*** Restoring submodule commits" + echo "" echo "*** Installing soldeer dependencies" rm -rf dependencies/* && forge soldeer update echo " Done" + +echo "" +echo "*** Applying patch to splits-waterfall" +patch -d dependencies/splits-waterfall-1.0.0/ -p1 < script/patch/splits_waterfall.patch diff --git a/script/patch/splits_waterfall.patch b/script/patch/splits_waterfall.patch new file mode 100644 index 00000000..591e7093 --- /dev/null +++ b/script/patch/splits_waterfall.patch @@ -0,0 +1,30 @@ +diff --git a/src/WaterfallModule.sol b/src/WaterfallModule.sol +index deb5c09..0429e4d 100644 +--- a/src/WaterfallModule.sol ++++ b/src/WaterfallModule.sol +@@ -1,9 +1,9 @@ + // SPDX-License-Identifier: GPL-3.0-or-later + pragma solidity ^0.8.17; + +-import {Clone} from "solady/utils/Clone.sol"; +-import {ERC20} from "solmate/tokens/ERC20.sol"; +-import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; ++import {Clone} from "@solady-0.0.124/utils/Clone.sol"; ++import {ERC20} from "@solmate-6.7.0/tokens/ERC20.sol"; ++import {SafeTransferLib} from "@solady-0.0.124/utils/SafeTransferLib.sol"; + + /// @title WaterfallModule + /// @author 0xSplits +diff --git a/src/WaterfallModuleFactory.sol b/src/WaterfallModuleFactory.sol +index 9fd7bd0..c9ac86a 100644 +--- a/src/WaterfallModuleFactory.sol ++++ b/src/WaterfallModuleFactory.sol +@@ -2,7 +2,7 @@ + pragma solidity ^0.8.17; + + import {WaterfallModule} from "./WaterfallModule.sol"; +-import {LibClone} from "solady/utils/LibClone.sol"; ++import {LibClone} from "@solady-0.0.124/utils/LibClone.sol"; + + /// @title WaterfallModuleFactory + /// @author 0xSplits diff --git a/script/patch/splits_waterfall.sh b/script/patch/splits_waterfall.sh new file mode 100755 index 00000000..40ef457e --- /dev/null +++ b/script/patch/splits_waterfall.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Move into the right directory +cd dependencies/splits-waterfall-1.0.0/ + +# Generate the diff +git diff . > ../../script/patch/splits_waterfall.patch + +echo "Done!" diff --git a/soldeer.lock b/soldeer.lock index 9d7a637f..99a5ea27 100644 --- a/soldeer.lock +++ b/soldeer.lock @@ -64,3 +64,9 @@ name = "solady" version = "0.0.124" source = "https://soldeer-revisions.s3.amazonaws.com/solady/0_0_124_22-01-2024_13:28:04_solady.zip" checksum = "9342385eaad08f9bb5408be0b41b241dd2b974c001f7da8c3b1ac552b52ce16b" + +[[dependencies]] +name = "splits-waterfall" +version = "1.0.0" +source = "git@github.com:0xSplits/splits-waterfall.git" +checksum = "1491f8b9454f22cf8a7bf5b384f5cdb75c9105b1" diff --git a/src/callbacks/splits/FixedFeePayment.sol b/src/callbacks/splits/FixedFeePayment.sol new file mode 100644 index 00000000..a5fef43f --- /dev/null +++ b/src/callbacks/splits/FixedFeePayment.sol @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +// Libraries +import {ERC20} from "@solmate-6.7.0/tokens/ERC20.sol"; +import {SafeTransferLib} from "@solmate-6.7.0/utils/SafeTransferLib.sol"; + +// Axis callback contracts +import {BaseCallback} from "@axis-core-1.0.0/bases/BaseCallback.sol"; +import {Callbacks} from "@axis-core-1.0.0/lib/Callbacks.sol"; + +// Splits contracts +import {WaterfallModuleFactory} from "@splits-waterfall-1.0.0/WaterfallModuleFactory.sol"; +import {WaterfallModule} from "@splits-waterfall-1.0.0/WaterfallModule.sol"; + +/// @title FixedFeePayment +/// @notice A callback that makes fixed fee payments to the specified recipients, after which the remaining balance is sent to the last recipient. +/// @dev This contract uses the Splits Waterfall contract, instead of reproducing the functionality. +contract FixedFeePayment is BaseCallback { + using SafeTransferLib for ERC20; + + // ========== ERRORS ========== // + + // ========== DATA STRUCTURES ========== // + + /// @notice Parameters for the WaterfallModule contract + /// @param recipients Addresses to waterfall payments to. The array should be one longer than `thresholds`, as residual payments are sent to the last recipient + /// @param thresholds Absolute payment thresholds for waterfall recipients + struct WaterfallParams { + address[] recipients; + uint256[] thresholds; + } + + // ========== STATE VARIABLES ========== // + + /// @notice The factory contract for deploying WaterfallModule clones + WaterfallModuleFactory public immutable factory; + + /// @notice Stores the WaterfallModule contract for each lot + mapping(uint96 lotId => WaterfallModule) public lotModules; + + /// @notice Stores the quote token for each lot + mapping(uint96 lotId => ERC20) public lotQuoteTokens; + + // ========== CONSTRUCTOR ========== // + + /// @dev This function reverts if: + /// - An unsupported permission is specified + /// - The callback is not configured to receive quote tokens + constructor( + address auctionHouse_, + Callbacks.Permissions memory permissions_, + address factory_ + ) BaseCallback(auctionHouse_, permissions_) { + // Validate that no unsupported permissions are specified + if (permissions_.onCancel || permissions_.onCurate || permissions_.onBid) { + revert Callback_InvalidParams(); + } + + // Validate that the callback is configured to receive proceeds + if (!permissions_.receiveQuoteTokens) { + revert Callback_InvalidParams(); + } + + // Validate that the factory contract is not the zero address + if (factory_ == address(0)) { + revert Callback_InvalidParams(); + } + + // Set the factory contract + factory = WaterfallModuleFactory(factory_); + } + + // ========== CALLBACK FUNCTIONS ========== // + + /// @inheritdoc BaseCallback + /// @dev This function performs the following: + /// - Validates the configuration parameters + /// - Creates a new Splits Waterfall contract + /// + /// This function reverts if: + /// - A Splits contract already exists for the lot + /// - The callback data cannot be decoded into a WaterfallParams struct + /// - Validation of the parameters by WaterfallModuleFactory fails + /// + /// Notes: + /// - The Splits Waterfall contract supports a fallback address for any other tokens that are sent to the contract. This is set to the seller's address. + function _onCreate( + uint96 lotId_, + address seller_, + address, + address quoteToken_, + uint256, + bool, + bytes calldata callbackData_ + ) internal virtual override { + // Validate if the Splits contract already exists for the lot + if (lotModules[lotId_] != WaterfallModule(address(0))) { + revert Callback_InvalidParams(); + } + + // Decode the callback data + WaterfallParams memory params = abi.decode(callbackData_, (WaterfallParams)); + + // Create the WaterfallModule + WaterfallModule wm = factory.createWaterfallModule( + quoteToken_, seller_, params.recipients, params.thresholds + ); + + // Store configuration + lotModules[lotId_] = wm; + lotQuoteTokens[lotId_] = ERC20(quoteToken_); + } + + /// @inheritdoc BaseCallback + /// @dev Not implemented + function _onCancel(uint96, uint256, bool, bytes calldata) internal virtual override { + revert Callback_NotImplemented(); + } + + /// @inheritdoc BaseCallback + /// @dev Not implemented + function _onCurate(uint96, uint256, bool, bytes calldata) internal virtual override { + revert Callback_NotImplemented(); + } + + /// @inheritdoc BaseCallback + /// @dev This function performs the following: + /// - Performs validation + /// - Transfers the proceeds to the Splits contract + /// - Calls the Splits contract to allocate the tokens to the recipients + /// + /// This function reverts if: + /// - The Splits contract for the lot does not exist + /// + /// Notes: + /// - The `WaterfallModule.waterfallFundsPull()` function is called to allocate the tokens to the recipients but not transfer them. This is to avoid errors. + /// - Funds can be withdrawn by calling `WaterfallModule.withdraw()` on the Splits contract. + function _onPurchase( + uint96 lotId_, + address, + uint256 amount_, + uint256, + bool, + bytes calldata + ) internal virtual override hasModule(lotId_) { + WaterfallModule wm = lotModules[lotId_]; + + // Transfer to the Splits contract + lotQuoteTokens[lotId_].safeTransfer(address(wm), amount_); + + // Allocate tokens + wm.waterfallFundsPull(); + } + + /// @inheritdoc BaseCallback + /// @dev Not implemented + function _onBid(uint96, uint64, address, uint256, bytes calldata) internal virtual override { + revert Callback_NotImplemented(); + } + + /// @inheritdoc BaseCallback + /// @dev This function performs the following: + /// - Performs validation + /// - Transfers the proceeds to the Splits contract + /// - Calls the Splits contract to allocate the tokens to the recipients + /// + /// This function reverts if: + /// - The Splits contract for the lot does not exist + /// + /// Notes: + /// - The `WaterfallModule.waterfallFundsPull()` function is called to allocate the tokens to the recipients but not transfer them. This is to avoid errors. + /// - Funds can be withdrawn by calling `WaterfallModule.withdraw()` on the Splits contract. + function _onSettle( + uint96 lotId_, + uint256 proceeds_, + uint256, + bytes calldata + ) internal virtual override hasModule(lotId_) { + // Transfer to the Splits contract + lotQuoteTokens[lotId_].safeTransfer(address(lotModules[lotId_]), proceeds_); + + // Allocate tokens + lotModules[lotId_].waterfallFundsPull(); + } + + // ========== MODIFIERS ========== // + + /// @notice Modifier to check if a WaterfallModule exists for the lot + modifier hasModule(uint96 lotId_) { + // Validate the module + if (lotModules[lotId_] == WaterfallModule(address(0))) { + revert Callback_InvalidParams(); + } + + // Validate the quote token + if (lotQuoteTokens[lotId_] == ERC20(address(0))) { + revert Callback_InvalidParams(); + } + _; + } +}