diff --git a/.gitmodules b/.gitmodules index 47b574d..4761d1e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,6 +10,7 @@ [submodule "lib/splits-oracle"] path = lib/splits-oracle url = https://github.com/0xSplits/splits-oracle + branch = move-discount-to-swapper [submodule "lib/v3-periphery"] path = lib/v3-periphery url = https://github.com/Uniswap/v3-periphery @@ -17,3 +18,4 @@ [submodule "lib/splits-utils"] path = lib/splits-utils url = https://github.com/0xsplits/splits-utils + branch = move-discount-to-swapper diff --git a/lib/forge-std b/lib/forge-std index 73d44ec..7f7cb06 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 73d44ec7d124e3831bc5f832267889ffb6f9bc3f +Subproject commit 7f7cb06aa55fc75d2280d7d54713dc4368d71fd3 diff --git a/lib/solady b/lib/solady index e0626f6..58681e7 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit e0626f63d46afda587439f93f6194570af06bf36 +Subproject commit 58681e79de23082fd3881a76022e0842f5c08db8 diff --git a/lib/splits-oracle b/lib/splits-oracle index 5d7f851..7d84d77 160000 --- a/lib/splits-oracle +++ b/lib/splits-oracle @@ -1 +1 @@ -Subproject commit 5d7f851aaca403d3d43f313dd9e98f16bb73fd51 +Subproject commit 7d84d77b7cd1424f3aaca58baef54c3f3082b8f9 diff --git a/lib/splits-utils b/lib/splits-utils index 750602e..52dbe4f 160000 --- a/lib/splits-utils +++ b/lib/splits-utils @@ -1 +1 @@ -Subproject commit 750602e561d65598730c42e37b956161bb6d3f3c +Subproject commit 52dbe4f918b09b7a53306de9c41b53615ae3acaa diff --git a/src/SwapperFactory.sol b/src/SwapperFactory.sol index c7e33aa..bcfbc22 100644 --- a/src/SwapperFactory.sol +++ b/src/SwapperFactory.sol @@ -22,6 +22,8 @@ contract SwapperFactory { address beneficiary; address tokenToBeneficiary; OracleParams oracleParams; + uint32 defaultScaledOfferFactor; + SwapperImpl.SetPairScaledOfferFactorParams[] pairScaledOfferFactors; } SwapperImpl public immutable swapperImpl; @@ -43,7 +45,9 @@ contract SwapperFactory { paused: params_.paused, beneficiary: params_.beneficiary, tokenToBeneficiary: params_.tokenToBeneficiary, - oracle: oracle + oracle: oracle, + defaultScaledOfferFactor: params_.defaultScaledOfferFactor, + pairScaledOfferFactors: params_.pairScaledOfferFactors }); swapper.initializer(swapperInitParams); diff --git a/src/SwapperImpl.sol b/src/SwapperImpl.sol index 3d3fe69..5490478 100644 --- a/src/SwapperImpl.sol +++ b/src/SwapperImpl.sol @@ -4,12 +4,14 @@ pragma solidity ^0.8.17; import {ERC20} from "solmate/tokens/ERC20.sol"; import {IOracle} from "splits-oracle/interfaces/IOracle.sol"; import {PausableImpl} from "splits-utils/PausableImpl.sol"; +import {QuotePair, QuoteParams, SortedQuotePair} from "splits-utils/LibQuotes.sol"; import {SafeCastLib} from "solady/utils/SafeCastLib.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {TokenUtils} from "splits-utils/TokenUtils.sol"; import {WalletImpl} from "splits-utils/WalletImpl.sol"; import {ISwapperFlashCallback} from "./interfaces/ISwapperFlashCallback.sol"; +import {PairScaledOfferFactors} from "./libraries/PairScaledOfferFactors.sol"; /// @title Swapper Implementation /// @author 0xSplits @@ -17,8 +19,8 @@ import {ISwapperFlashCallback} from "./interfaces/ISwapperFlashCallback.sol"; /// onchain revenue into a single token /// Please be aware, owner has _FULL CONTROL_ of the deployment. /// @dev This contract uses a modular oracle. Be very careful to use a secure -/// oracle with sensible defaults & overrides for desired behavior. Otherwise -/// may result in catastrophic loss of funds. +/// oracle with sensible settings for the desired behavior. Insecure oracles +/// will result in catastrophic loss of funds. /// This contract uses token = address(0) to refer to ETH. contract SwapperImpl is WalletImpl, PausableImpl { /// ----------------------------------------------------------------------- @@ -28,6 +30,7 @@ contract SwapperImpl is WalletImpl, PausableImpl { using SafeTransferLib for address; using SafeCastLib for uint256; using TokenUtils for address; + using PairScaledOfferFactors for mapping(address => mapping(address => uint32)); /// ----------------------------------------------------------------------- /// errors @@ -48,6 +51,13 @@ contract SwapperImpl is WalletImpl, PausableImpl { address beneficiary; address tokenToBeneficiary; IOracle oracle; + uint32 defaultScaledOfferFactor; + SetPairScaledOfferFactorParams[] pairScaledOfferFactors; + } + + struct SetPairScaledOfferFactorParams { + QuotePair quotePair; + uint32 scaledOfferFactor; } /// ----------------------------------------------------------------------- @@ -57,12 +67,14 @@ contract SwapperImpl is WalletImpl, PausableImpl { event SetBeneficiary(address beneficiary); event SetTokenToBeneficiary(address tokenToBeneficiary); event SetOracle(IOracle oracle); + event SetDefaultScaledOfferFactor(uint32 defaultScaledOfferFactor); + event SetPairScaledOfferFactors(SetPairScaledOfferFactorParams[] params); event ReceiveETH(uint256 amount); event Payback(address indexed payer, uint256 amount); event Flash( address indexed trader, - IOracle.QuoteParams[] quoteParams, + QuoteParams[] quoteParams, address tokenToBeneficiary, uint256[] amountsToBeneficiary, uint256 excessToBeneficiary @@ -78,6 +90,9 @@ contract SwapperImpl is WalletImpl, PausableImpl { address public immutable swapperFactory; + /// @dev percentages measured in hundredths of basis points + uint32 internal constant PERCENTAGE_SCALE = 100_00_00; // = 100% + /// ----------------------------------------------------------------------- /// storage - mutables /// ----------------------------------------------------------------------- @@ -102,19 +117,31 @@ contract SwapperImpl is WalletImpl, PausableImpl { uint96 internal $_payback; /// 12 bytes - /// slot 2 - 12 bytes free + /// slot 2 - 8 bytes free /// token type to send beneficiary /// @dev 0x0 used for ETH address internal $tokenToBeneficiary; /// 20 bytes + /// default price scaling factor + /// @dev PERCENTAGE_SCALE = 1e6 = 100_00_00 = 100% = no discount or premium + /// 99_00_00 = 99% = 1% discount to oracle; 101_00_00 = 101% = 1% premium to oracle + /// 4 bytes + uint32 internal $defaultScaledOfferFactor; + /// slot 3 - 12 bytes free /// price oracle for flash IOracle internal $oracle; /// 20 bytes + /// slot 4 - 0 bytes free + + /// scaledOfferFactors for specific quote pairs + /// 32 bytes + mapping(address => mapping(address => uint32)) internal $_pairScaledOfferFactors; + /// ----------------------------------------------------------------------- /// constructor & initializer /// ----------------------------------------------------------------------- @@ -133,6 +160,9 @@ contract SwapperImpl is WalletImpl, PausableImpl { $beneficiary = params_.beneficiary; $tokenToBeneficiary = params_.tokenToBeneficiary; $oracle = params_.oracle; + $defaultScaledOfferFactor = params_.defaultScaledOfferFactor; + + $_pairScaledOfferFactors._set(params_.pairScaledOfferFactors); } /// ----------------------------------------------------------------------- @@ -165,6 +195,18 @@ contract SwapperImpl is WalletImpl, PausableImpl { emit SetOracle(oracle_); } + /// set defaultScaledOfferFactor + function setDefaultScaledOfferFactor(uint32 defaultScaledOfferFactor_) external onlyOwner { + $defaultScaledOfferFactor = defaultScaledOfferFactor_; + emit SetDefaultScaledOfferFactor(defaultScaledOfferFactor_); + } + + /// set pair scaled offer factors + function setPairScaledOfferFactors(SetPairScaledOfferFactorParams[] calldata params_) external onlyOwner { + $_pairScaledOfferFactors._set(params_); + emit SetPairScaledOfferFactors(params_); + } + /// ----------------------------------------------------------------------- /// functions - public & external - view /// ----------------------------------------------------------------------- @@ -181,6 +223,26 @@ contract SwapperImpl is WalletImpl, PausableImpl { return $oracle; } + function defaultScaledOfferFactor() external view returns (uint32) { + return $defaultScaledOfferFactor; + } + + /// get pair scaled offer factors for an array of quote pairs + function getPairScaledOfferFactors(QuotePair[] calldata quotePairs_) + external + view + returns (uint32[] memory pairScaledOfferFactors) + { + uint256 length = quotePairs_.length; + pairScaledOfferFactors = new uint32[](length); + for (uint256 i; i < length;) { + pairScaledOfferFactors[i] = $_pairScaledOfferFactors._get(quotePairs_[i]); + unchecked { + ++i; + } + } + } + /// ----------------------------------------------------------------------- /// functions - public & external - permissionless /// ----------------------------------------------------------------------- @@ -200,11 +262,7 @@ contract SwapperImpl is WalletImpl, PausableImpl { } /// allow third parties to withdraw tokens in return for sending tokenToBeneficiary to beneficiary - function flash(IOracle.QuoteParams[] calldata quoteParams_, bytes calldata callbackData_) - external - payable - pausable - { + function flash(QuoteParams[] calldata quoteParams_, bytes calldata callbackData_) external pausable { address _tokenToBeneficiary = $tokenToBeneficiary; (uint256 amountToBeneficiary, uint256[] memory amountsToBeneficiary) = _transferToTrader(_tokenToBeneficiary, quoteParams_); @@ -224,18 +282,20 @@ contract SwapperImpl is WalletImpl, PausableImpl { /// functions - private & internal /// ----------------------------------------------------------------------- - function _transferToTrader(address tokenToBeneficiary_, IOracle.QuoteParams[] calldata quoteParams_) + function _transferToTrader(address tokenToBeneficiary_, QuoteParams[] calldata quoteParams_) internal returns (uint256 amountToBeneficiary, uint256[] memory amountsToBeneficiary) { - amountsToBeneficiary = $oracle.getQuoteAmounts(quoteParams_); + uint256[] memory unscaledAmountsToBeneficiary = $oracle.getQuoteAmounts(quoteParams_); uint256 length = quoteParams_.length; - if (amountsToBeneficiary.length != length) revert Invalid_AmountsToBeneficiary(); + if (unscaledAmountsToBeneficiary.length != length) revert Invalid_AmountsToBeneficiary(); + amountsToBeneficiary = new uint256[](length); + uint256 scaledAmountToBeneficiary; uint128 amountToTrader; address tokenToTrader; for (uint256 i; i < length;) { - IOracle.QuoteParams calldata qp = quoteParams_[i]; + QuoteParams calldata qp = quoteParams_[i]; if (tokenToBeneficiary_ != qp.quotePair.quote) revert Invalid_QuoteToken(); tokenToTrader = qp.quotePair.base; @@ -245,7 +305,14 @@ contract SwapperImpl is WalletImpl, PausableImpl { revert InsufficientFunds_InContract(); } - amountToBeneficiary += amountsToBeneficiary[i]; + uint32 scaledOfferFactor = $_pairScaledOfferFactors._get(qp.quotePair._sort()); + if (scaledOfferFactor == 0) { + scaledOfferFactor = $defaultScaledOfferFactor; + } + + scaledAmountToBeneficiary = unscaledAmountsToBeneficiary[i] * scaledOfferFactor / PERCENTAGE_SCALE; + amountsToBeneficiary[i] = scaledAmountToBeneficiary; + amountToBeneficiary += scaledAmountToBeneficiary; tokenToTrader._safeTransfer(msg.sender, amountToTrader); unchecked { diff --git a/src/integrations/UniV3Swap.sol b/src/integrations/UniV3Swap.sol index ad0403c..a7fbd57 100644 --- a/src/integrations/UniV3Swap.sol +++ b/src/integrations/UniV3Swap.sol @@ -5,6 +5,7 @@ import {ERC20} from "solmate/tokens/ERC20.sol"; import {IOracle} from "splits-oracle/interfaces/IOracle.sol"; import {ISwapRouter} from "v3-periphery/interfaces/ISwapRouter.sol"; import {IWETH9} from "splits-utils/interfaces/external/IWETH9.sol"; +import {QuoteParams} from "splits-utils/LibQuotes.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {TokenUtils} from "splits-utils/TokenUtils.sol"; @@ -24,7 +25,7 @@ contract UniV3Swap is ISwapperFlashCallback { error InsufficientFunds(); struct InitFlashParams { - IOracle.QuoteParams[] quoteParams; + QuoteParams[] quoteParams; FlashCallbackData flashCallbackData; } diff --git a/src/libraries/PairScaledOfferFactors.sol b/src/libraries/PairScaledOfferFactors.sol new file mode 100644 index 0000000..2916e16 --- /dev/null +++ b/src/libraries/PairScaledOfferFactors.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.17; + +import {QuotePair, SortedQuotePair} from "splits-utils/LibQuotes.sol"; + +import {SwapperImpl} from "../SwapperImpl.sol"; + +/// @title PairScaledOfferFactor Library +/// @author 0xSplits +/// @notice Setters & getters for quote pairs' scaledOfferFactors +library PairScaledOfferFactors { + /// set pairs' scaled offer factors + function _set( + mapping(address => mapping(address => uint32)) storage self, + SwapperImpl.SetPairScaledOfferFactorParams[] calldata params_ + ) internal { + uint256 length = params_.length; + for (uint256 i; i < length;) { + _set(self, params_[i]); + unchecked { + ++i; + } + } + } + + /// set pair's scaled offer factor + function _set( + mapping(address => mapping(address => uint32)) storage self, + SwapperImpl.SetPairScaledOfferFactorParams calldata params_ + ) internal { + SortedQuotePair memory sqp = params_.quotePair._sort(); + self[sqp.token0][sqp.token1] = params_.scaledOfferFactor; + } + + /// get pair's scaled offer factor + function _get(mapping(address => mapping(address => uint32)) storage self, QuotePair calldata quotePair_) + internal + view + returns (uint32) + { + return _get(self, quotePair_._sort()); + } + + /// get pair's scaled offer factor + function _get(mapping(address => mapping(address => uint32)) storage self, SortedQuotePair memory sqp_) + internal + view + returns (uint32) + { + return self[sqp_.token0][sqp_.token1]; + } +} diff --git a/test/SwapperFactory.t.sol b/test/SwapperFactory.t.sol index d1d616c..20a9885 100644 --- a/test/SwapperFactory.t.sol +++ b/test/SwapperFactory.t.sol @@ -6,6 +6,7 @@ import {LibCloneBase} from "splits-tests/LibClone.t.sol"; import {CreateOracleParams, IOracleFactory, IOracle, OracleParams} from "splits-oracle/peripherals/OracleParams.sol"; import {IUniswapV3Factory, UniV3OracleFactory} from "splits-oracle/UniV3OracleFactory.sol"; +import {QuotePair} from "splits-utils/LibQuotes.sol"; import {UniV3OracleImpl} from "splits-oracle/UniV3OracleImpl.sol"; import {SwapperFactory} from "../src/SwapperFactory.sol"; @@ -14,17 +15,23 @@ import {SwapperImpl} from "../src/SwapperImpl.sol"; // TODO: add fuzz tests contract SwapperFactoryTest is BaseTest, LibCloneBase { - event CreateSwapper(SwapperImpl indexed swapper, SwapperImpl.InitParams params); + event CreateSwapper(SwapperImpl indexed swapper, SwapperImpl.InitParams initSwapperParams); SwapperFactory swapperFactory; SwapperImpl swapperImpl; - SwapperFactory.CreateSwapperParams params; - SwapperImpl.InitParams swapperInitParams; + address beneficiary; + address owner; + bool paused; + address tokenToBeneficiary; + uint32 defaultScaledOfferFactor; + SwapperImpl.SetPairScaledOfferFactorParams[] pairScaledOfferFactors; UniV3OracleFactory oracleFactory; - UniV3OracleImpl.SetPairOverrideParams[] pairOverrides; + uint24 defaultFee; + uint32 defaultPeriod; + UniV3OracleImpl.SetPairOverrideParams[] oraclePairOverrides; CreateOracleParams createOracleParams; OracleParams oracleParams; IOracle oracle; @@ -32,6 +39,19 @@ contract SwapperFactoryTest is BaseTest, LibCloneBase { function setUp() public virtual override(BaseTest, LibCloneBase) { BaseTest.setUp(); + owner = users.alice; + beneficiary = users.bob; + paused = false; + tokenToBeneficiary = ETH_ADDRESS; + defaultScaledOfferFactor = 99_00_00; + + pairScaledOfferFactors.push( + SwapperImpl.SetPairScaledOfferFactorParams({ + quotePair: QuotePair({base: WETH9, quote: ETH_ADDRESS}), + scaledOfferFactor: PERCENTAGE_SCALE // no discount + }) + ); + // set oracle up oracleFactory = new UniV3OracleFactory({ uniswapV3Factory_: IUniswapV3Factory(UNISWAP_V3_FACTORY), @@ -40,6 +60,9 @@ contract SwapperFactoryTest is BaseTest, LibCloneBase { // TODO: add pair override? + defaultFee = 30_00; // = 0.3% + defaultPeriod = 30 minutes; + UniV3OracleImpl.InitParams memory initOracleParams = _initOracleParams(); createOracleParams = @@ -53,29 +76,47 @@ contract SwapperFactoryTest is BaseTest, LibCloneBase { swapperFactory = new SwapperFactory(); swapperImpl = swapperFactory.swapperImpl(); - params = SwapperFactory.CreateSwapperParams({ - owner: users.alice, - paused: false, - beneficiary: users.bob, - tokenToBeneficiary: ETH_ADDRESS, - oracleParams: oracleParams - }); - - swapperInitParams = SwapperImpl.InitParams({ - owner: users.alice, - paused: false, - beneficiary: users.bob, - tokenToBeneficiary: ETH_ADDRESS, - oracle: oracle - }); - // setup LibCloneBase impl = address(swapperImpl); - clone = address(swapperFactory.createSwapper(params)); + clone = address(swapperFactory.createSwapper(_createSwapperParams())); amount = 1 ether; data = "Hello, World!"; } + function _createSwapperParams() internal view returns (SwapperFactory.CreateSwapperParams memory) { + return SwapperFactory.CreateSwapperParams({ + owner: owner, + paused: paused, + beneficiary: beneficiary, + tokenToBeneficiary: tokenToBeneficiary, + oracleParams: oracleParams, + defaultScaledOfferFactor: defaultScaledOfferFactor, + pairScaledOfferFactors: pairScaledOfferFactors + }); + } + + function _initSwapperParams() internal view returns (SwapperImpl.InitParams memory) { + return SwapperImpl.InitParams({ + owner: owner, + paused: paused, + beneficiary: beneficiary, + tokenToBeneficiary: tokenToBeneficiary, + oracle: oracle, + defaultScaledOfferFactor: defaultScaledOfferFactor, + pairScaledOfferFactors: pairScaledOfferFactors + }); + } + + function _initOracleParams() internal view returns (UniV3OracleImpl.InitParams memory) { + return UniV3OracleImpl.InitParams({ + owner: owner, + paused: paused, + defaultFee: defaultFee, + defaultPeriod: defaultPeriod, + pairOverrides: oraclePairOverrides + }); + } + /// ----------------------------------------------------------------------- /// createSwapper /// ----------------------------------------------------------------------- @@ -84,121 +125,95 @@ contract SwapperFactoryTest is BaseTest, LibCloneBase { vm.expectCall({ callee: address(swapperImpl), msgValue: 0 ether, - data: abi.encodeCall( - SwapperImpl.initializer, - ( - SwapperImpl.InitParams({ - owner: users.alice, - paused: false, - beneficiary: users.bob, - tokenToBeneficiary: ETH_ADDRESS, - oracle: oracle - }) - ) - ) + data: abi.encodeCall(SwapperImpl.initializer, (_initSwapperParams())) }); - swapperFactory.createSwapper(params); + swapperFactory.createSwapper(_createSwapperParams()); } function test_createSwapper_emitsCreateSwapper() public { SwapperImpl expectedSwapper = SwapperImpl(_predictNextAddressFrom(address(swapperFactory))); _expectEmit(); - emit CreateSwapper(expectedSwapper, swapperInitParams); - swapperFactory.createSwapper(params); + emit CreateSwapper(expectedSwapper, _initSwapperParams()); + swapperFactory.createSwapper(_createSwapperParams()); } function test_createSwapper_createsOracleIfNotProvidedOne() public { - params.oracleParams.oracle = IOracle(ADDRESS_ZERO); + SwapperFactory.CreateSwapperParams memory createSwapperParams = _createSwapperParams(); + + createSwapperParams.oracleParams.oracle = IOracle(ADDRESS_ZERO); vm.expectCall({ callee: address(oracleFactory), msgValue: 0 ether, data: abi.encodeCall(IOracleFactory.createOracle, abi.encode(_initOracleParams())) }); - swapperFactory.createSwapper(params); + swapperFactory.createSwapper(createSwapperParams); } function testFuzz_createSwapper_createsClone_code( - SwapperFactory.CreateSwapperParams calldata params_, + SwapperFactory.CreateSwapperParams calldata createSwapperParams_, address newOracle_ ) public { vm.mockCall({ - callee: address(params_.oracleParams.createOracleParams.factory), + callee: address(createSwapperParams_.oracleParams.createOracleParams.factory), msgValue: 0, - data: abi.encodeCall(IOracleFactory.createOracle, (params_.oracleParams.createOracleParams.data)), + data: abi.encodeCall(IOracleFactory.createOracle, (createSwapperParams_.oracleParams.createOracleParams.data)), returnData: abi.encode(newOracle_) }); - clone = address(swapperFactory.createSwapper(params_)); + clone = address(swapperFactory.createSwapper(createSwapperParams_)); test_clone_code(); } function testFuzz_createSwapper_createsClone_canReceiveETH( - SwapperFactory.CreateSwapperParams calldata params_, + SwapperFactory.CreateSwapperParams calldata createSwapperParams_, address newOracle_, uint96 amount_ ) public { vm.mockCall({ - callee: address(params_.oracleParams.createOracleParams.factory), + callee: address(createSwapperParams_.oracleParams.createOracleParams.factory), msgValue: 0, - data: abi.encodeCall(IOracleFactory.createOracle, (params_.oracleParams.createOracleParams.data)), + data: abi.encodeCall(IOracleFactory.createOracle, (createSwapperParams_.oracleParams.createOracleParams.data)), returnData: abi.encode(newOracle_) }); - clone = address(swapperFactory.createSwapper(params_)); + clone = address(swapperFactory.createSwapper(createSwapperParams_)); amount = amount_; test_clone_canReceiveETH(); } function testFuzz_createSwapper_createsClone_emitsReceiveETH( - SwapperFactory.CreateSwapperParams calldata params_, + SwapperFactory.CreateSwapperParams calldata createSwapperParams_, address newOracle_, uint96 amount_ ) public { vm.mockCall({ - callee: address(params_.oracleParams.createOracleParams.factory), + callee: address(createSwapperParams_.oracleParams.createOracleParams.factory), msgValue: 0, - data: abi.encodeCall(IOracleFactory.createOracle, (params_.oracleParams.createOracleParams.data)), + data: abi.encodeCall(IOracleFactory.createOracle, (createSwapperParams_.oracleParams.createOracleParams.data)), returnData: abi.encode(newOracle_) }); - clone = address(swapperFactory.createSwapper(params_)); + clone = address(swapperFactory.createSwapper(createSwapperParams_)); amount = amount_; test_clone_emitsReceiveETH(); } function testFuzz_createSwapper_createsClone_canDelegateCall( - SwapperFactory.CreateSwapperParams calldata params_, + SwapperFactory.CreateSwapperParams calldata createSwapperParams_, address newOracle_, bytes calldata data_ ) public { vm.assume(data_.length > 0); vm.mockCall({ - callee: address(params_.oracleParams.createOracleParams.factory), + callee: address(createSwapperParams_.oracleParams.createOracleParams.factory), msgValue: 0, - data: abi.encodeCall(IOracleFactory.createOracle, (params_.oracleParams.createOracleParams.data)), + data: abi.encodeCall(IOracleFactory.createOracle, (createSwapperParams_.oracleParams.createOracleParams.data)), returnData: abi.encode(newOracle_) }); - clone = address(swapperFactory.createSwapper(params_)); + clone = address(swapperFactory.createSwapper(createSwapperParams_)); data = data_; test_clone_canDelegateCall(); } - - /// ----------------------------------------------------------------------- - /// internal - /// ----------------------------------------------------------------------- - - /// @dev can't be init'd in setUp & saved to storage bc of nested dynamic array solc error - /// UnimplementedFeatureError: Copying of type struct UniV3OracleImpl.SetPairOverrideParams memory[] memory to storage not yet supported. - function _initOracleParams() internal view returns (UniV3OracleImpl.InitParams memory) { - return UniV3OracleImpl.InitParams({ - owner: users.alice, - paused: false, - defaultFee: 30_00, // = 0.3% - defaultPeriod: 30 minutes, - defaultScaledOfferFactor: PERCENTAGE_SCALE, - pairOverrides: pairOverrides - }); - } } diff --git a/test/SwapperImpl.t.sol b/test/SwapperImpl.t.sol index 2e79e62..3a492be 100644 --- a/test/SwapperImpl.t.sol +++ b/test/SwapperImpl.t.sol @@ -4,14 +4,20 @@ pragma solidity ^0.8.17; import "splits-tests/Base.t.sol"; import {IUniswapV3Factory, UniV3OracleFactory} from "splits-oracle/UniV3OracleFactory.sol"; -import {IOracle, QuotePair} from "splits-oracle/interfaces/IOracle.sol"; +import {IOracle} from "splits-oracle/interfaces/IOracle.sol"; import {OracleParams} from "splits-oracle/peripherals/OracleParams.sol"; +import {QuotePair, QuoteParams} from "splits-utils/LibQuotes.sol"; import {UniV3OracleImpl} from "splits-oracle/UniV3OracleImpl.sol"; import {ISwapperFlashCallback} from "../src/interfaces/ISwapperFlashCallback.sol"; import {SwapperFactory} from "../src/SwapperFactory.sol"; import {SwapperImpl} from "../src/SwapperImpl.sol"; +// TODO: add test for scaling override ? +// TODO: add flash test for weth-weth +// TODO: add flash test for eth-weth +// TODO: add flash test for eth-eth + // TODO: separate file of tests for integration contract // TODO: add fuzzing @@ -29,12 +35,14 @@ contract SwapperImplTest is BaseTest { event SetBeneficiary(address beneficiary); event SetTokenToBeneficiary(address tokenToBeneficiaryd); event SetOracle(IOracle oracle); + event SetDefaultScaledOfferFactor(uint32 defaultScaledOfferFactor); + event SetPairScaledOfferFactors(SwapperImpl.SetPairScaledOfferFactorParams[] params); event ReceiveETH(uint256 amount); event Payback(address indexed payer, uint256 amount); event Flash( address indexed trader, - IOracle.QuoteParams[] quoteParams, + QuoteParams[] quoteParams, address tokenToBeneficiary, uint256[] amountsToBeneficiary, uint256 excessToBeneficiary @@ -45,24 +53,30 @@ contract SwapperImplTest is BaseTest { SwapperImpl swapperImpl; SwapperImpl swapper; - SwapperFactory.CreateSwapperParams createSwapperParams; - SwapperImpl.InitParams initParams; - UniV3OracleFactory oracleFactory; UniV3OracleImpl.InitParams initOracleParams; IOracle oracle; OracleParams oracleParams; - IOracle.QuoteParams[] ethQuoteParams; - IOracle.QuoteParams[] mockERC20QuoteParams; + QuoteParams[] ethQuoteParams; + QuoteParams[] mockERC20QuoteParams; uint256[] mockQuoteAmounts; + QuotePair wethETH; + QuotePair usdcETH; + address trader; - address beneficiary; - IOracle.QuoteParams[] quoteParams; - IOracle.QuoteParams qp; + address beneficiary; + address owner; + bool paused; + address tokenToBeneficiary; + uint32 defaultScaledOfferFactor; + SwapperImpl.SetPairScaledOfferFactorParams[] setPairScaledOfferFactorParams; + + QuoteParams[] quoteParams; + QuoteParams qp; address base; address quote; @@ -91,38 +105,38 @@ contract SwapperImplTest is BaseTest { swapperFactory = new SwapperFactory(); swapperImpl = swapperFactory.swapperImpl(); + owner = users.alice; beneficiary = users.bob; + paused = false; + tokenToBeneficiary = ETH_ADDRESS; + defaultScaledOfferFactor = 99_00_00; - createSwapperParams = SwapperFactory.CreateSwapperParams({ - owner: users.alice, - paused: false, - beneficiary: beneficiary, - tokenToBeneficiary: ETH_ADDRESS, - oracleParams: oracleParams - }); - initParams = SwapperImpl.InitParams({ - owner: users.alice, - paused: false, - beneficiary: beneficiary, - tokenToBeneficiary: ETH_ADDRESS, - oracle: oracle - }); - swapper = swapperFactory.createSwapper(createSwapperParams); + wethETH = QuotePair({base: WETH9, quote: ETH_ADDRESS}); + usdcETH = QuotePair({base: USDC, quote: ETH_ADDRESS}); + + setPairScaledOfferFactorParams.push( + SwapperImpl.SetPairScaledOfferFactorParams({ + quotePair: wethETH, + scaledOfferFactor: PERCENTAGE_SCALE // no discount + }) + ); + + swapper = swapperFactory.createSwapper(_createSwapperParams()); _deal({account: address(swapper)}); swapperImplHarness = new SwapperImplHarness(); - swapperImplHarness.initializer(initParams); + swapperImplHarness.initializer(_initSwapperParams()); _deal({account: address(swapperImplHarness)}); ethQuoteParams.push( - IOracle.QuoteParams({ + QuoteParams({ quotePair: QuotePair({base: address(mockERC20), quote: ETH_ADDRESS}), baseAmount: 1 ether, data: "" }) ); mockERC20QuoteParams.push( - IOracle.QuoteParams({ + QuoteParams({ quotePair: QuotePair({base: ETH_ADDRESS, quote: address(mockERC20)}), baseAmount: 1 ether, data: "" @@ -161,6 +175,30 @@ contract SwapperImplTest is BaseTest { }); } + function _initSwapperParams() internal view returns (SwapperImpl.InitParams memory) { + return SwapperImpl.InitParams({ + owner: owner, + paused: paused, + beneficiary: beneficiary, + tokenToBeneficiary: tokenToBeneficiary, + oracle: oracle, + defaultScaledOfferFactor: defaultScaledOfferFactor, + pairScaledOfferFactors: setPairScaledOfferFactorParams + }); + } + + function _createSwapperParams() internal view returns (SwapperFactory.CreateSwapperParams memory) { + return SwapperFactory.CreateSwapperParams({ + owner: owner, + paused: paused, + beneficiary: beneficiary, + tokenToBeneficiary: tokenToBeneficiary, + oracleParams: oracleParams, + defaultScaledOfferFactor: defaultScaledOfferFactor, + pairScaledOfferFactors: setPairScaledOfferFactorParams + }); + } + /// ----------------------------------------------------------------------- /// modifiers /// ----------------------------------------------------------------------- @@ -186,42 +224,78 @@ contract SwapperImplTest is BaseTest { /// ----------------------------------------------------------------------- function test_revertWhen_callerNotFactory_initializer() public { + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + vm.expectRevert(Unauthorized.selector); - swapperImpl.initializer(initParams); + swapperImpl.initializer(initSwapperParams); vm.expectRevert(Unauthorized.selector); - swapper.initializer(initParams); + swapper.initializer(initSwapperParams); } function test_initializer_setsOwner() public callerFactory { + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + vm.prank(address(swapperFactory)); - swapper.initializer(initParams); - assertEq(swapper.owner(), initParams.owner); + swapper.initializer(initSwapperParams); + assertEq(swapper.owner(), initSwapperParams.owner); } function test_initializer_setsPaused() public callerFactory { + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + vm.prank(address(swapperFactory)); - swapper.initializer(initParams); - assertEq(swapper.paused(), initParams.paused); + swapper.initializer(initSwapperParams); + assertEq(swapper.paused(), initSwapperParams.paused); } function test_initializer_setsBeneficiary() public callerFactory { + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + vm.prank(address(swapperFactory)); - swapper.initializer(initParams); - assertEq(swapper.beneficiary(), initParams.beneficiary); + swapper.initializer(initSwapperParams); + assertEq(swapper.beneficiary(), initSwapperParams.beneficiary); } function test_initializer_setsTokenToBeneficiary() public callerFactory { + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + + vm.prank(address(swapperFactory)); + swapper.initializer(initSwapperParams); + assertEq(swapper.tokenToBeneficiary(), initSwapperParams.tokenToBeneficiary); + } + + function test_initializer_setsDefaultScaledOfferFactor() public callerFactory { + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + vm.prank(address(swapperFactory)); - swapper.initializer(initParams); - assertEq(swapper.tokenToBeneficiary(), initParams.tokenToBeneficiary); + swapper.initializer(initSwapperParams); + assertEq(swapper.defaultScaledOfferFactor(), initSwapperParams.defaultScaledOfferFactor); + } + + function test_initializer_setsPairScaledOfferFactors() public callerFactory { + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + + vm.prank(address(swapperFactory)); + swapper.initializer(initSwapperParams); + + uint256 length = initSwapperParams.pairScaledOfferFactors.length; + QuotePair[] memory initQuotePairs = new QuotePair[](length); + uint32[] memory initScaledOfferFactors = new uint32[](length); + for (uint256 i; i < length; i++) { + initQuotePairs[i] = initSwapperParams.pairScaledOfferFactors[i].quotePair; + initScaledOfferFactors[i] = initSwapperParams.pairScaledOfferFactors[i].scaledOfferFactor; + } + assertEq(swapper.getPairScaledOfferFactors(initQuotePairs), initScaledOfferFactors); } function test_initializer_emitsOwnershipTransferred() public callerFactory { + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + vm.prank(address(swapperFactory)); _expectEmit(); - emit OwnershipTransferred(address(0), initParams.owner); - swapper.initializer(initParams); + emit OwnershipTransferred(address(0), initSwapperParams.owner); + swapper.initializer(initSwapperParams); } /// ----------------------------------------------------------------------- @@ -234,13 +308,17 @@ contract SwapperImplTest is BaseTest { } function test_setBeneficiary_setsBeneficiary() public callerOwner { - vm.prank(initParams.owner); + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + + vm.prank(initSwapperParams.owner); swapper.setBeneficiary(users.eve); assertEq(swapper.beneficiary(), users.eve); } function test_setBeneficiary_emitsSetBeneficiary() public callerOwner { - vm.prank(initParams.owner); + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + + vm.prank(initSwapperParams.owner); vm.expectEmit(); emit SetBeneficiary(users.eve); swapper.setBeneficiary(users.eve); @@ -256,13 +334,17 @@ contract SwapperImplTest is BaseTest { } function test_setTokenToBeneficiary_setsTokenToBeneficiary() public callerOwner { - vm.prank(initParams.owner); + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + + vm.prank(initSwapperParams.owner); swapper.setTokenToBeneficiary(users.eve); assertEq(swapper.tokenToBeneficiary(), users.eve); } function test_setTokenToBeneficiary_emitsSetTokenToBeneficiary() public callerOwner { - vm.prank(initParams.owner); + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + + vm.prank(initSwapperParams.owner); vm.expectEmit(); emit SetTokenToBeneficiary(users.eve); swapper.setTokenToBeneficiary(users.eve); @@ -278,18 +360,103 @@ contract SwapperImplTest is BaseTest { } function test_setOracle_setsOracle() public callerOwner { - vm.prank(initParams.owner); + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + + vm.prank(initSwapperParams.owner); swapper.setOracle(IOracle(users.eve)); assertEq(address(swapper.oracle()), users.eve); } function test_setOracle_emitsSetOracle() public callerOwner { - vm.prank(initParams.owner); + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + + vm.prank(initSwapperParams.owner); vm.expectEmit(); emit SetOracle(IOracle(users.eve)); swapper.setOracle(IOracle(users.eve)); } + /// ----------------------------------------------------------------------- + /// tests - basic - setDefaultScaledOfferFactor + /// ----------------------------------------------------------------------- + + function test_revertWhen_callerNotOwner_setDefaultScaledOfferFactor() public { + uint32 newDefaultScaledOfferFactor = 98_00_00; + vm.expectRevert(Unauthorized.selector); + swapper.setDefaultScaledOfferFactor(newDefaultScaledOfferFactor); + } + + function test_setDefaultScaledOfferFactor_setsDefaultScaledOfferFactor() public callerOwner { + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + uint32 newDefaultScaledOfferFactor = 98_00_00; + + vm.prank(initSwapperParams.owner); + swapper.setDefaultScaledOfferFactor(newDefaultScaledOfferFactor); + assertEq(swapper.defaultScaledOfferFactor(), newDefaultScaledOfferFactor); + } + + function test_setDefaultScaledOfferFactor_emitsSetDefaultScaledOfferFactor() public callerOwner { + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + uint32 newDefaultScaledOfferFactor = 98_00_00; + + vm.prank(initSwapperParams.owner); + vm.expectEmit(); + emit SetDefaultScaledOfferFactor(newDefaultScaledOfferFactor); + swapper.setDefaultScaledOfferFactor(newDefaultScaledOfferFactor); + } + + /// ----------------------------------------------------------------------- + /// tests - basic - setPairScaledOfferFactors + /// ----------------------------------------------------------------------- + + function test_revertWhen_callerNotOwner_setPairScaledOfferFactors() public { + vm.expectRevert(Unauthorized.selector); + swapper.setPairScaledOfferFactors(setPairScaledOfferFactorParams); + } + + function test_setPairScaledOfferFactors_setsPairScaledOfferFactors() public callerOwner { + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + + delete setPairScaledOfferFactorParams; + setPairScaledOfferFactorParams.push( + SwapperImpl.SetPairScaledOfferFactorParams({quotePair: wethETH, scaledOfferFactor: 0}) + ); + setPairScaledOfferFactorParams.push( + SwapperImpl.SetPairScaledOfferFactorParams({quotePair: usdcETH, scaledOfferFactor: 98_00_00}) + ); + uint256 length = setPairScaledOfferFactorParams.length; + + vm.prank(initSwapperParams.owner); + swapper.setPairScaledOfferFactors(setPairScaledOfferFactorParams); + + QuotePair[] memory quotePairs = new QuotePair[](length); + uint32[] memory newScaledOfferFactors = new uint32[](length); + for (uint256 i; i < length; i++) { + quotePairs[i] = setPairScaledOfferFactorParams[i].quotePair; + newScaledOfferFactors[i] = setPairScaledOfferFactorParams[i].scaledOfferFactor; + } + assertEq(swapper.getPairScaledOfferFactors(quotePairs), newScaledOfferFactors); + } + + function test_setPairScaledOfferFactors_emitsSetPairScaledOfferFactors() public callerOwner { + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + + // TODO: use setup? + + delete setPairScaledOfferFactorParams; + setPairScaledOfferFactorParams.push( + SwapperImpl.SetPairScaledOfferFactorParams({quotePair: wethETH, scaledOfferFactor: 0}) + ); + setPairScaledOfferFactorParams.push( + SwapperImpl.SetPairScaledOfferFactorParams({quotePair: usdcETH, scaledOfferFactor: 98_00_00}) + ); + + vm.prank(initSwapperParams.owner); + vm.expectEmit(); + emit SetPairScaledOfferFactors(setPairScaledOfferFactorParams); + swapper.setPairScaledOfferFactors(setPairScaledOfferFactorParams); + } + /// ----------------------------------------------------------------------- /// tests - basic - payback /// ----------------------------------------------------------------------- @@ -310,7 +477,9 @@ contract SwapperImplTest is BaseTest { /// ----------------------------------------------------------------------- function test_revertWhen_paused_flash() public { - vm.prank(initParams.owner); + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + + vm.prank(initSwapperParams.owner); swapper.setPaused(true); vm.expectRevert(Paused.selector); @@ -320,36 +489,41 @@ contract SwapperImplTest is BaseTest { function test_flash_mockERC20ToETH() public unpaused { vm.startPrank(trader); + uint256 value = 1 ether * uint256(defaultScaledOfferFactor) / PERCENTAGE_SCALE; + vm.mockCall({ callee: trader, msgValue: 0, - data: abi.encodeCall(ISwapperFlashCallback.swapperFlashCallback, (quote, 1 ether, "")), + data: abi.encodeCall(ISwapperFlashCallback.swapperFlashCallback, (quote, value, "")), returnData: "" }); - swapper.payback{value: 1 ether}(); + swapper.payback{value: value}(); swapper.flash(quoteParams, ""); assertEq(base._balanceOf(trader), traderBasePreBalance + 1 ether); assertEq(base._balanceOf(address(swapper)), swapperBasePreBalance - 1 ether); assertEq(base._balanceOf(beneficiary), beneficiaryBasePreBalance); - assertEq(quote._balanceOf(trader), traderQuotePreBalance - 1 ether); + assertEq(quote._balanceOf(trader), traderQuotePreBalance - value); assertEq(quote._balanceOf(address(swapper)), 0); - assertEq(quote._balanceOf(beneficiary), beneficiaryQuotePreBalance + swapperQuotePreBalance + 1 ether); + assertEq(quote._balanceOf(beneficiary), beneficiaryQuotePreBalance + swapperQuotePreBalance + value); } function test_flash_mockERC20ToETH_emitsFlash() public unpaused { vm.startPrank(trader); + uint256 value = 1 ether * uint256(defaultScaledOfferFactor) / PERCENTAGE_SCALE; + vm.mockCall({ callee: trader, msgValue: 0, - data: abi.encodeCall(ISwapperFlashCallback.swapperFlashCallback, (quote, 1 ether, "")), + data: abi.encodeCall(ISwapperFlashCallback.swapperFlashCallback, (quote, value, "")), returnData: "" }); - swapper.payback{value: 1 ether}(); + swapper.payback{value: value}(); + mockQuoteAmounts[0] = value; _expectEmit(); emit Flash({ @@ -363,9 +537,13 @@ contract SwapperImplTest is BaseTest { } function test_flash_ethToMockERC20() public unpaused { - initParams.tokenToBeneficiary = mockERC20; + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + + uint256 value = 1 ether * uint256(defaultScaledOfferFactor) / PERCENTAGE_SCALE; + + initSwapperParams.tokenToBeneficiary = mockERC20; vm.prank(address(swapperFactory)); - swapper.initializer(initParams); + swapper.initializer(initSwapperParams); vm.startPrank(trader); @@ -382,12 +560,12 @@ contract SwapperImplTest is BaseTest { swapperQuotePreBalance = quote._balanceOf(address(swapper)); beneficiaryQuotePreBalance = quote._balanceOf(beneficiary); - MockERC20(mockERC20).approve(address(swapper), 1 ether); + MockERC20(mockERC20).approve(address(swapper), value); vm.mockCall({ callee: trader, msgValue: 0, - data: abi.encodeCall(ISwapperFlashCallback.swapperFlashCallback, (quote, 1 ether, "")), + data: abi.encodeCall(ISwapperFlashCallback.swapperFlashCallback, (quote, value, "")), returnData: "" }); @@ -397,15 +575,19 @@ contract SwapperImplTest is BaseTest { assertEq(base._balanceOf(address(swapper)), swapperBasePreBalance - 1 ether); assertEq(base._balanceOf(beneficiary), beneficiaryBasePreBalance); - assertEq(quote._balanceOf(trader), traderQuotePreBalance - 1 ether); + assertEq(quote._balanceOf(trader), traderQuotePreBalance - value); assertEq(quote._balanceOf(address(swapper)), 0); - assertEq(quote._balanceOf(beneficiary), beneficiaryQuotePreBalance + swapperQuotePreBalance + 1 ether); + assertEq(quote._balanceOf(beneficiary), beneficiaryQuotePreBalance + swapperQuotePreBalance + value); } function test_flash_ethToMockERC20_emitsFlash() public unpaused { - initParams.tokenToBeneficiary = mockERC20; + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + + uint256 value = 1 ether * uint256(defaultScaledOfferFactor) / PERCENTAGE_SCALE; + + initSwapperParams.tokenToBeneficiary = mockERC20; vm.prank(address(swapperFactory)); - swapper.initializer(initParams); + swapper.initializer(initSwapperParams); vm.startPrank(trader); @@ -416,15 +598,16 @@ contract SwapperImplTest is BaseTest { swapperQuotePreBalance = quote._balanceOf(address(swapper)); - MockERC20(mockERC20).approve(address(swapper), 1 ether); + MockERC20(mockERC20).approve(address(swapper), value); vm.mockCall({ callee: trader, msgValue: 0, - data: abi.encodeCall(ISwapperFlashCallback.swapperFlashCallback, (quote, 1 ether, "")), + data: abi.encodeCall(ISwapperFlashCallback.swapperFlashCallback, (quote, value, "")), returnData: "" }); + mockQuoteAmounts[0] = value; _expectEmit(); emit Flash({ trader: trader, @@ -532,6 +715,94 @@ contract SwapperImplTest is BaseTest { /// ----------------------------------------------------------------------- /// tests - fuzz /// ----------------------------------------------------------------------- + + /// ----------------------------------------------------------------------- + /// tests - fuzz - setDefaultScaledOfferFactor + /// ----------------------------------------------------------------------- + + function testFuzz_revertWhen_callerNotOwner_setDefaultScaledOfferFactor( + address notOwner_, + uint32 newDefaultScaledOfferFactor_ + ) public { + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + + vm.assume(notOwner_ != initSwapperParams.owner); + vm.prank(notOwner_); + vm.expectRevert(Unauthorized.selector); + swapper.setDefaultScaledOfferFactor(newDefaultScaledOfferFactor_); + } + + function testFuzz_setDefaultScaledOfferFactor_setsDefaultScaledOfferFactor(uint32 newDefaultScaledOfferFactor_) + public + callerOwner + { + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + + vm.prank(initSwapperParams.owner); + swapper.setDefaultScaledOfferFactor(newDefaultScaledOfferFactor_); + assertEq(swapper.defaultScaledOfferFactor(), newDefaultScaledOfferFactor_); + } + + function testFuzz_setDefaultScaledOfferFactor_emitsSetDefaultScaledOfferFactor(uint32 newDefaultScaledOfferFactor_) + public + callerOwner + { + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + + vm.prank(initSwapperParams.owner); + vm.expectEmit(); + emit SetDefaultScaledOfferFactor(newDefaultScaledOfferFactor_); + swapper.setDefaultScaledOfferFactor(newDefaultScaledOfferFactor_); + } + + /// ----------------------------------------------------------------------- + /// tests - fuzz - setPairScaledOfferFactors + /// ----------------------------------------------------------------------- + + function testFuzz_revertWhen_callerNotOwner_setPairScaledOfferFactors( + address notOwner_, + SwapperImpl.SetPairScaledOfferFactorParams[] memory newSetPairScaledOfferFactors_ + ) public { + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + + vm.assume(notOwner_ != initSwapperParams.owner); + vm.prank(notOwner_); + vm.expectRevert(Unauthorized.selector); + swapper.setPairScaledOfferFactors(newSetPairScaledOfferFactors_); + } + + // TODO: upgrade to test array; need to prune converted duplicates + function testFuzz_setPairScaledOfferFactors_setsPairScaledOfferFactors( + SwapperImpl.SetPairScaledOfferFactorParams memory newSetPairScaledOfferFactors_ + ) public callerOwner { + uint256 length = 1; + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + SwapperImpl.SetPairScaledOfferFactorParams[] memory newSetPairScaledOfferFactors = + new SwapperImpl.SetPairScaledOfferFactorParams[](1); + newSetPairScaledOfferFactors[0] = newSetPairScaledOfferFactors_; + + vm.prank(initSwapperParams.owner); + swapper.setPairScaledOfferFactors(newSetPairScaledOfferFactors); + + QuotePair[] memory quotePairs = new QuotePair[](length); + uint32[] memory newScaledOfferFactors = new uint32[](length); + for (uint256 i; i < length; i++) { + quotePairs[i] = newSetPairScaledOfferFactors[i].quotePair; + newScaledOfferFactors[i] = newSetPairScaledOfferFactors[i].scaledOfferFactor; + } + assertEq(swapper.getPairScaledOfferFactors(quotePairs), newScaledOfferFactors); + } + + function testFuzz_setPairScaledOfferFactors_emitsSetPairScaledOfferFactors( + SwapperImpl.SetPairScaledOfferFactorParams[] memory newSetPairScaledOfferFactors_ + ) public callerOwner { + SwapperImpl.InitParams memory initSwapperParams = _initSwapperParams(); + + vm.prank(initSwapperParams.owner); + vm.expectEmit(); + emit SetPairScaledOfferFactors(newSetPairScaledOfferFactors_); + swapper.setPairScaledOfferFactors(newSetPairScaledOfferFactors_); + } } contract SwapperImplHarness is SwapperImpl { @@ -539,7 +810,7 @@ contract SwapperImplHarness is SwapperImpl { return $_payback; } - function exposed_transferToTrader(address tokenToBeneficiary_, IOracle.QuoteParams[] calldata quoteParams_) + function exposed_transferToTrader(address tokenToBeneficiary_, QuoteParams[] calldata quoteParams_) external returns (uint256 amountToBeneficiary, uint256[] memory amountsToBeneficiary) {