diff --git a/.forge-snapshots/settler_metaTxn_uniswapV3_USDT-WETH.snap b/.forge-snapshots/settler_metaTxn_uniswapV3_USDT-WETH.snap index af6427320..a7c9b9614 100644 --- a/.forge-snapshots/settler_metaTxn_uniswapV3_USDT-WETH.snap +++ b/.forge-snapshots/settler_metaTxn_uniswapV3_USDT-WETH.snap @@ -1 +1 @@ -160735 \ No newline at end of file +160734 \ No newline at end of file diff --git a/.forge-snapshots/zeroEx_uniswapV3VIP_multiplex1_USDC-WETH.snap b/.forge-snapshots/zeroEx_uniswapV3VIP_multiplex1_USDC-WETH.snap index 9fcfa14af..80c0300cd 100644 --- a/.forge-snapshots/zeroEx_uniswapV3VIP_multiplex1_USDC-WETH.snap +++ b/.forge-snapshots/zeroEx_uniswapV3VIP_multiplex1_USDC-WETH.snap @@ -1 +1 @@ -138683 \ No newline at end of file +138686 \ No newline at end of file diff --git a/.forge-snapshots/zeroEx_uniswapV3VIP_multiplex2_USDC-WETH.snap b/.forge-snapshots/zeroEx_uniswapV3VIP_multiplex2_USDC-WETH.snap index dee14e418..a25bd5209 100644 --- a/.forge-snapshots/zeroEx_uniswapV3VIP_multiplex2_USDC-WETH.snap +++ b/.forge-snapshots/zeroEx_uniswapV3VIP_multiplex2_USDC-WETH.snap @@ -1 +1 @@ -181465 \ No newline at end of file +181468 \ No newline at end of file diff --git a/README.md b/README.md index 58f2801c7..5edb84986 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Note: The following is more akin to `gasLimit` than it is `gasUsed`, this is due | VIP | DEX | Pair | Gas | % | | ------------------- | ---------- | --------- | ------ | ------ | | 0x V4 VIP | Uniswap V3 | USDC/WETH | 125117 | 0.00% | -| 0x V4 Multiplex | Uniswap V3 | USDC/WETH | 138683 | 10.84% | +| 0x V4 Multiplex | Uniswap V3 | USDC/WETH | 138686 | 10.85% | | Settler VIP (warm) | Uniswap V3 | USDC/WETH | 134857 | 7.78% | | AllowanceHolder VIP | Uniswap V3 | USDC/WETH | 130694 | 4.46% | | UniswapRouter V3 | Uniswap V3 | USDC/WETH | 121137 | -3.18% | @@ -73,7 +73,7 @@ Note: The following is more akin to `gasLimit` than it is `gasUsed`, this is due | Settler | Uniswap V3 | DAI/WETH | 154057 | -36.05% | | | | | | | | 0x V4 Multiplex | Uniswap V3 | USDT/WETH | 243700 | 0.00% | -| Settler | Uniswap V3 | USDT/WETH | 160735 | -34.04% | +| Settler | Uniswap V3 | USDT/WETH | 160734 | -34.04% | | | | | | | | OTC | DEX | Pair | Gas | % | diff --git a/foundry.toml b/foundry.toml index d27d48a91..ade537e6d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,7 +8,7 @@ no_match_path = "*/integration/*" fuzz_runs = 10000 # needed for marktoda/forge-gas-snapshot ffi = true -fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}] +fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"},{ access = "read", path = "out"}] evm_version = "shanghai" [profile.integration] diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index 0e546aba3..000000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "forge-std/Script.sol"; - -contract CounterScript is Script { - function setUp() public {} - - function run() public { - vm.broadcast(); - } -} diff --git a/src/core/Basic.sol b/src/core/Basic.sol index e313e6c00..76e792db9 100644 --- a/src/core/Basic.sol +++ b/src/core/Basic.sol @@ -26,12 +26,14 @@ abstract contract Basic is Permit2PaymentAbstract { revert ConfusedDeputy(); } + bool success; + bytes memory returnData; uint256 value; if (sellToken == IERC20(ETH_ADDRESS)) { value = address(this).balance.mulDiv(bips, 10_000); if (data.length == 0) { if (offset != 0) revert InvalidOffset(); - (bool success, bytes memory returnData) = payable(pool).call{value: value}(""); + (success, returnData) = payable(pool).call{value: value}(""); success.maybeRevert(returnData); return; } else { @@ -56,7 +58,7 @@ abstract contract Basic is Permit2PaymentAbstract { sellToken.safeApproveIfBelow(pool, amount); } } - (bool success, bytes memory returnData) = payable(pool).call{value: value}(data); + (success, returnData) = payable(pool).call{value: value}(data); success.maybeRevert(returnData); // forbid sending data to EOAs if (returnData.length == 0 && pool.code.length == 0) revert InvalidTarget(); diff --git a/src/core/OtcOrderSettlement.sol b/src/core/OtcOrderSettlement.sol index 7de18bca0..9502ca193 100644 --- a/src/core/OtcOrderSettlement.sol +++ b/src/core/OtcOrderSettlement.sol @@ -19,12 +19,12 @@ abstract contract OtcOrderSettlement is SettlerAbstract { bool partialFillAllowed; } - /// @dev Emitted whenever an OTC order is filled. + /// @dev Emitted whenever an Otc order is filled. /// @param orderHash The canonical hash of the order. Formed as an EIP712 struct hash. See below. /// @param maker The maker of the order. /// @param taker The taker of the order. - /// @param makerTokenFilledAmount How much maker token was filled. - /// @param takerTokenFilledAmount How much taker token was filled. + /// @param makerTokenFilledAmount Amount of maker token filled. + /// @param takerTokenFilledAmount Amount of taker token filled. event OtcOrderFilled( bytes32 indexed orderHash, address maker, @@ -79,9 +79,8 @@ abstract contract OtcOrderSettlement is SettlerAbstract { } /// @dev Settle an OtcOrder between maker and taker transfering funds directly between - /// the counterparties. Two Permit2 signatures are consumed, with the maker Permit2 containing - /// a witness of the OtcOrder. - /// This variant also includes a fee where the taker or maker pays the fee recipient + /// the counterparties. Either two Permit2 signatures are consumed, with the maker Permit2 containing + /// a witness of the OtcOrder, or AllowanceHolder is supported for the taker payment. function fillOtcOrder( address recipient, ISignatureTransfer.PermitTransferFrom memory makerPermit, @@ -101,12 +100,12 @@ abstract contract OtcOrderSettlement is SettlerAbstract { bytes32 witness = _hashConsideration(consideration); // There is no taker witness (see below) - // Maker pays recipient (optional fee) + // Maker pays recipient _transferFrom(makerPermit, makerTransferDetails, maker, witness, CONSIDERATION_WITNESS, makerSig, false); - // Taker pays Maker (optional fee) + // Taker pays Maker // We don't need to include a witness here. Taker is `_msgSender()`, so // `recipient` and the maker's details are already authenticated. We're just - // using PERMIT2 to move tokens, not to provide authentication. + // using Permit2 or AllowanceHolder to move tokens, not to provide authentication. _transferFrom(takerPermit, takerTransferDetails, _msgSender(), takerSig); emit OtcOrderFilled( @@ -128,6 +127,7 @@ abstract contract OtcOrderSettlement is SettlerAbstract { /// @dev Settle an OtcOrder between maker and taker transfering funds directly between /// the counterparties. Both Maker and Taker have signed the same order, and submission /// is via a third party + /// @dev `takerWitness` is not calculated nor verified here as caller is trusted function fillOtcOrderMetaTxn( address recipient, ISignatureTransfer.PermitTransferFrom memory makerPermit, @@ -151,6 +151,7 @@ abstract contract OtcOrderSettlement is SettlerAbstract { makerConsideration.counterparty = taker; bytes32 makerWitness = _hashConsideration(makerConsideration); + // Note: takerWitness is not calculated here, but in the caller code _transferFrom(makerPermit, makerTransferDetails, maker, makerWitness, CONSIDERATION_WITNESS, makerSig, false); _transferFrom(takerPermit, takerTransferDetails, taker, takerWitness, ACTIONS_AND_SLIPPAGE_WITNESS, takerSig); @@ -169,7 +170,7 @@ abstract contract OtcOrderSettlement is SettlerAbstract { /// @dev Settle an OtcOrder between maker and Settler retaining funds in this contract. /// @dev pre-condition: msgSender has been authenticated against the requestor /// One Permit2 signature is consumed, with the maker Permit2 containing a witness of the OtcOrder. - // In this variant, Maker pays Settler and Settler pays Maker + // In this variant, Maker pays recipient and Settler pays Maker function fillOtcOrderSelfFunded( address recipient, ISignatureTransfer.PermitTransferFrom memory permit, diff --git a/src/core/Permit2Payment.sol b/src/core/Permit2Payment.sol index 472fcf947..1d3288c73 100644 --- a/src/core/Permit2Payment.sol +++ b/src/core/Permit2Payment.sol @@ -47,12 +47,6 @@ abstract contract Permit2PaymentAbstract is ContextAbstract { function isRestrictedTarget(address) internal view virtual returns (bool); - function _permitToTransferDetails(ISignatureTransfer.PermitBatchTransferFrom memory permit, address recipient) - internal - view - virtual - returns (ISignatureTransfer.SignatureTransferDetails[] memory transferDetails, address token, uint256 amount); - function _permitToTransferDetails(ISignatureTransfer.PermitTransferFrom memory permit, address recipient) internal pure @@ -60,8 +54,8 @@ abstract contract Permit2PaymentAbstract is ContextAbstract { returns (ISignatureTransfer.SignatureTransferDetails memory transferDetails, address token, uint256 amount); function _transferFrom( - ISignatureTransfer.PermitBatchTransferFrom memory permit, - ISignatureTransfer.SignatureTransferDetails[] memory transferDetails, + ISignatureTransfer.PermitTransferFrom memory permit, + ISignatureTransfer.SignatureTransferDetails memory transferDetails, address from, bytes32 witness, string memory witnessTypeString, @@ -70,8 +64,8 @@ abstract contract Permit2PaymentAbstract is ContextAbstract { ) internal virtual; function _transferFrom( - ISignatureTransfer.PermitBatchTransferFrom memory permit, - ISignatureTransfer.SignatureTransferDetails[] memory transferDetails, + ISignatureTransfer.PermitTransferFrom memory permit, + ISignatureTransfer.SignatureTransferDetails memory transferDetails, address from, bytes32 witness, string memory witnessTypeString, @@ -82,8 +76,6 @@ abstract contract Permit2PaymentAbstract is ContextAbstract { ISignatureTransfer.PermitTransferFrom memory permit, ISignatureTransfer.SignatureTransferDetails memory transferDetails, address from, - bytes32 witness, - string memory witnessTypeString, bytes memory sig, bool isForwarded ) internal virtual; @@ -92,15 +84,31 @@ abstract contract Permit2PaymentAbstract is ContextAbstract { ISignatureTransfer.PermitTransferFrom memory permit, ISignatureTransfer.SignatureTransferDetails memory transferDetails, address from, - bytes32 witness, - string memory witnessTypeString, bytes memory sig ) internal virtual; +} + +/// @dev Batch support for Permit2 payments +/// === WARNING: UNUSED === +abstract contract Permit2BatchPaymentAbstract is ContextAbstract { + string internal constant TOKEN_PERMISSIONS_TYPE = "TokenPermissions(address token,uint256 amount)"; + + error FeeTokenMismatch(address paymentToken, address feeToken); + + function isRestrictedTarget(address) internal view virtual returns (bool); + + function _permitToTransferDetails(ISignatureTransfer.PermitBatchTransferFrom memory permit, address recipient) + internal + view + virtual + returns (ISignatureTransfer.SignatureTransferDetails[] memory transferDetails, address token, uint256 amount); function _transferFrom( ISignatureTransfer.PermitBatchTransferFrom memory permit, ISignatureTransfer.SignatureTransferDetails[] memory transferDetails, address from, + bytes32 witness, + string memory witnessTypeString, bytes memory sig, bool isForwarded ) internal virtual; @@ -109,38 +117,31 @@ abstract contract Permit2PaymentAbstract is ContextAbstract { ISignatureTransfer.PermitBatchTransferFrom memory permit, ISignatureTransfer.SignatureTransferDetails[] memory transferDetails, address from, + bytes32 witness, + string memory witnessTypeString, bytes memory sig ) internal virtual; function _transferFrom( - ISignatureTransfer.PermitTransferFrom memory permit, - ISignatureTransfer.SignatureTransferDetails memory transferDetails, + ISignatureTransfer.PermitBatchTransferFrom memory permit, + ISignatureTransfer.SignatureTransferDetails[] memory transferDetails, address from, bytes memory sig, bool isForwarded ) internal virtual; function _transferFrom( - ISignatureTransfer.PermitTransferFrom memory permit, - ISignatureTransfer.SignatureTransferDetails memory transferDetails, + ISignatureTransfer.PermitBatchTransferFrom memory permit, + ISignatureTransfer.SignatureTransferDetails[] memory transferDetails, address from, bytes memory sig ) internal virtual; } -abstract contract Permit2Payment is Permit2PaymentAbstract, AllowanceHolderContext { - using UnsafeMath for uint256; - using UnsafeArray for IAllowanceHolder.TransferDetails[]; - using UnsafeArray for ISignatureTransfer.TokenPermissions[]; - using UnsafeArray for ISignatureTransfer.SignatureTransferDetails[]; - +contract Permit2PaymentBase is AllowanceHolderContext { /// @dev Permit2 address - ISignatureTransfer private immutable _PERMIT2; - address private immutable _FEE_RECIPIENT; - - function isRestrictedTarget(address target) internal view override returns (bool) { - return target == address(_PERMIT2) || target == address(allowanceHolder); - } + ISignatureTransfer internal immutable _PERMIT2; + address internal immutable _FEE_RECIPIENT; constructor(address permit2, address feeRecipient, address allowanceHolder) AllowanceHolderContext(allowanceHolder) @@ -148,8 +149,21 @@ abstract contract Permit2Payment is Permit2PaymentAbstract, AllowanceHolderConte _PERMIT2 = ISignatureTransfer(permit2); _FEE_RECIPIENT = feeRecipient; } +} - error FeeTokenMismatch(address paymentToken, address feeToken); +abstract contract Permit2BatchPayment is Permit2PaymentBase, Permit2BatchPaymentAbstract { + using UnsafeMath for uint256; + using UnsafeArray for IAllowanceHolder.TransferDetails[]; + using UnsafeArray for ISignatureTransfer.TokenPermissions[]; + using UnsafeArray for ISignatureTransfer.SignatureTransferDetails[]; + + constructor(address permit2, address feeRecipient, address allowanceHolder) + Permit2PaymentBase(permit2, feeRecipient, allowanceHolder) + {} + + function isRestrictedTarget(address target) internal view override returns (bool) { + return target == address(_PERMIT2) || target == address(allowanceHolder); + } function _permitToTransferDetails(ISignatureTransfer.PermitBatchTransferFrom memory permit, address recipient) internal @@ -180,15 +194,52 @@ abstract contract Permit2Payment is Permit2PaymentAbstract, AllowanceHolderConte } } - function _permitToTransferDetails(ISignatureTransfer.PermitTransferFrom memory permit, address recipient) - internal - pure - override - returns (ISignatureTransfer.SignatureTransferDetails memory transferDetails, address token, uint256 amount) - { - transferDetails.to = recipient; - transferDetails.requestedAmount = amount = permit.permitted.amount; - token = permit.permitted.token; + function _transferFrom( + ISignatureTransfer.PermitBatchTransferFrom memory permit, + ISignatureTransfer.SignatureTransferDetails[] memory transferDetails, + address from, + bytes32 witness, + string memory witnessTypeString, + bytes memory sig, + bool isForwarded + ) internal override { + if (isForwarded) revert ForwarderNotAllowed(); + _PERMIT2.permitWitnessTransferFrom(permit, transferDetails, from, witness, witnessTypeString, sig); + } + + function _transferFrom( + ISignatureTransfer.PermitBatchTransferFrom memory permit, + ISignatureTransfer.SignatureTransferDetails[] memory transferDetails, + address from, + bytes32 witness, + string memory witnessTypeString, + bytes memory sig + ) internal override { + _transferFrom(permit, transferDetails, from, witness, witnessTypeString, sig, _isForwarded()); + } + + function _transferFrom( + ISignatureTransfer.PermitBatchTransferFrom memory permit, + ISignatureTransfer.SignatureTransferDetails[] memory transferDetails, + address from, + bytes memory sig, + bool isForwarded + ) internal override { + if (isForwarded) { + if (sig.length != 0) revert InvalidSignatureLen(); + allowanceHolder.holderTransferFrom(from, _formatForAllowanceHolder(permit, transferDetails)); + } else { + _PERMIT2.permitTransferFrom(permit, transferDetails, from, sig); + } + } + + function _transferFrom( + ISignatureTransfer.PermitBatchTransferFrom memory permit, + ISignatureTransfer.SignatureTransferDetails[] memory transferDetails, + address from, + bytes memory sig + ) internal override { + _transferFrom(permit, transferDetails, from, sig, _isForwarded()); } function _formatForAllowanceHolder( @@ -211,6 +262,32 @@ abstract contract Permit2Payment is Permit2PaymentAbstract, AllowanceHolderConte newDetail.amount = oldDetail.requestedAmount; } } +} + +abstract contract Permit2Payment is Permit2PaymentBase, Permit2PaymentAbstract { + using UnsafeMath for uint256; + using UnsafeArray for IAllowanceHolder.TransferDetails[]; + using UnsafeArray for ISignatureTransfer.TokenPermissions[]; + using UnsafeArray for ISignatureTransfer.SignatureTransferDetails[]; + + constructor(address permit2, address feeRecipient, address allowanceHolder) + Permit2PaymentBase(permit2, feeRecipient, allowanceHolder) + {} + + function isRestrictedTarget(address target) internal view override returns (bool) { + return target == address(_PERMIT2) || target == address(allowanceHolder); + } + + function _permitToTransferDetails(ISignatureTransfer.PermitTransferFrom memory permit, address recipient) + internal + pure + override + returns (ISignatureTransfer.SignatureTransferDetails memory transferDetails, address token, uint256 amount) + { + transferDetails.to = recipient; + transferDetails.requestedAmount = amount = permit.permitted.amount; + token = permit.permitted.token; + } function _formatForAllowanceHolder( ISignatureTransfer.PermitTransferFrom memory permit, @@ -223,30 +300,6 @@ abstract contract Permit2Payment is Permit2PaymentAbstract, AllowanceHolderConte newDetail.amount = transferDetails.requestedAmount; } - function _transferFrom( - ISignatureTransfer.PermitBatchTransferFrom memory permit, - ISignatureTransfer.SignatureTransferDetails[] memory transferDetails, - address from, - bytes32 witness, - string memory witnessTypeString, - bytes memory sig, - bool isForwarded - ) internal override { - if (isForwarded) revert ForwarderNotAllowed(); - _PERMIT2.permitWitnessTransferFrom(permit, transferDetails, from, witness, witnessTypeString, sig); - } - - function _transferFrom( - ISignatureTransfer.PermitBatchTransferFrom memory permit, - ISignatureTransfer.SignatureTransferDetails[] memory transferDetails, - address from, - bytes32 witness, - string memory witnessTypeString, - bytes memory sig - ) internal override { - _transferFrom(permit, transferDetails, from, witness, witnessTypeString, sig, _isForwarded()); - } - function _transferFrom( ISignatureTransfer.PermitTransferFrom memory permit, ISignatureTransfer.SignatureTransferDetails memory transferDetails, @@ -271,30 +324,6 @@ abstract contract Permit2Payment is Permit2PaymentAbstract, AllowanceHolderConte _transferFrom(permit, transferDetails, from, witness, witnessTypeString, sig, _isForwarded()); } - function _transferFrom( - ISignatureTransfer.PermitBatchTransferFrom memory permit, - ISignatureTransfer.SignatureTransferDetails[] memory transferDetails, - address from, - bytes memory sig, - bool isForwarded - ) internal override { - if (isForwarded) { - if (sig.length != 0) revert InvalidSignatureLen(); - allowanceHolder.holderTransferFrom(from, _formatForAllowanceHolder(permit, transferDetails)); - } else { - _PERMIT2.permitTransferFrom(permit, transferDetails, from, sig); - } - } - - function _transferFrom( - ISignatureTransfer.PermitBatchTransferFrom memory permit, - ISignatureTransfer.SignatureTransferDetails[] memory transferDetails, - address from, - bytes memory sig - ) internal override { - _transferFrom(permit, transferDetails, from, sig, _isForwarded()); - } - function _transferFrom( ISignatureTransfer.PermitTransferFrom memory permit, ISignatureTransfer.SignatureTransferDetails memory transferDetails, diff --git a/src/core/UniswapV2.sol b/src/core/UniswapV2.sol index 73ce8986c..9f8f95bf4 100644 --- a/src/core/UniswapV2.sol +++ b/src/core/UniswapV2.sol @@ -187,7 +187,7 @@ abstract contract UniswapV2 is VIPBase { // final swap if fromPool { - // perform swap at the fromPool sending bought tokens to settler + // perform swap at the fromPool sending bought tokens to recipient mstore(add(swapCalldata, 0x44), and(0xffffffffffffffffffffffffffffffffffffffff, recipient)) if iszero(call(gas(), fromPool, 0, swapCalldata, 0xa4, 0, 0)) { bubbleRevert(swapCalldata) } } diff --git a/test/unit/AllowanceHolderUnitTest.t.sol b/test/unit/AllowanceHolderUnitTest.t.sol index f4d8e5b21..91b8f1364 100644 --- a/test/unit/AllowanceHolderUnitTest.t.sol +++ b/test/unit/AllowanceHolderUnitTest.t.sol @@ -5,9 +5,11 @@ import {AllowanceHolder} from "../../src/AllowanceHolder.sol"; import {IAllowanceHolder} from "../../src/IAllowanceHolder.sol"; import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol"; +import {IERC20} from "../../src/IERC20.sol"; + +import {Utils} from "./Utils.sol"; import {Test} from "forge-std/Test.sol"; -import {VmSafe} from "forge-std/Vm.sol"; contract AllowanceHolderDummy is AllowanceHolder { function getAllowed(address operator, address owner, address token) external view returns (uint256 r) { @@ -19,16 +21,12 @@ contract AllowanceHolderDummy is AllowanceHolder { } } -contract FallbackDummy { - fallback() external payable {} -} - -contract AllowanceHolderUnitTest is Test { +contract AllowanceHolderUnitTest is Utils, Test { AllowanceHolderDummy ah; - address OPERATOR = address(0x01); - address TOKEN = address(0x02); + address OPERATOR = _createNamedRejectionDummy("OPERATOR"); + address TOKEN = _createNamedRejectionDummy("TOKEN"); address OWNER = address(this); - address RECIPIENT = address(0); + address RECIPIENT = _createNamedRejectionDummy("RECIPIENT"); uint256 AMOUNT = 123456; function setUp() public { @@ -41,62 +39,76 @@ contract AllowanceHolderUnitTest is Test { } function testPermitAuthorised() public { - address token = address(new FallbackDummy()); - address operator = address(this); - - ah.setAllowed(operator, OWNER, token, AMOUNT); IAllowanceHolder.TransferDetails[] memory transferDetails = new IAllowanceHolder.TransferDetails[](1); - transferDetails[0] = IAllowanceHolder.TransferDetails(token, RECIPIENT, AMOUNT); + transferDetails[0] = IAllowanceHolder.TransferDetails({token: TOKEN, recipient: RECIPIENT, amount: AMOUNT}); - assertEq(ah.getAllowed(operator, OWNER, token), AMOUNT); + ah.setAllowed(OPERATOR, OWNER, TOKEN, AMOUNT); + assertEq(ah.getAllowed(OPERATOR, OWNER, TOKEN), AMOUNT); + + _mockExpectCall( + TOKEN, abi.encodeWithSelector(IERC20.transferFrom.selector, OWNER, RECIPIENT, AMOUNT), new bytes(0) + ); + vm.prank(OPERATOR); assertTrue(ah.holderTransferFrom(OWNER, transferDetails)); - assertEq(ah.getAllowed(operator, OWNER, token), 0); + + assertEq(ah.getAllowed(OPERATOR, OWNER, TOKEN), 0); } function testPermitAuthorisedMultipleConsumption() public { - address token = address(new FallbackDummy()); - address operator = address(this); - - ah.setAllowed(operator, OWNER, token, AMOUNT); IAllowanceHolder.TransferDetails[] memory transferDetails = new IAllowanceHolder.TransferDetails[](1); - transferDetails[0] = IAllowanceHolder.TransferDetails(token, RECIPIENT, AMOUNT / 2); - assertEq(ah.getAllowed(operator, OWNER, token), AMOUNT); + // Note: we use amount / 2 (+ / - 1) to register multiple mocks + transferDetails[0] = + IAllowanceHolder.TransferDetails({token: TOKEN, recipient: RECIPIENT, amount: (AMOUNT / 2) + 1}); + + ah.setAllowed(OPERATOR, OWNER, TOKEN, AMOUNT); + assertEq(ah.getAllowed(OPERATOR, OWNER, TOKEN), AMOUNT); + _mockExpectCall( + TOKEN, + abi.encodeWithSelector(IERC20.transferFrom.selector, OWNER, RECIPIENT, (AMOUNT / 2) + 1), + new bytes(0) + ); + vm.prank(OPERATOR); assertTrue(ah.holderTransferFrom(OWNER, transferDetails)); - assertEq(ah.getAllowed(operator, OWNER, token), AMOUNT / 2); + assertEq(ah.getAllowed(OPERATOR, OWNER, TOKEN), (AMOUNT / 2) - 1); + + _mockExpectCall( + TOKEN, + abi.encodeWithSelector(IERC20.transferFrom.selector, OWNER, RECIPIENT, (AMOUNT / 2) - 1), + new bytes(0) + ); + transferDetails[0] = + IAllowanceHolder.TransferDetails({token: TOKEN, recipient: RECIPIENT, amount: (AMOUNT / 2) - 1}); + vm.prank(OPERATOR); assertTrue(ah.holderTransferFrom(OWNER, transferDetails)); - assertEq(ah.getAllowed(operator, OWNER, token), 0); + + assertEq(ah.getAllowed(OPERATOR, OWNER, TOKEN), 0); } function testPermitUnauthorisedOperator() public { - ah.setAllowed(OPERATOR, OWNER, TOKEN, AMOUNT); IAllowanceHolder.TransferDetails[] memory transferDetails = new IAllowanceHolder.TransferDetails[](1); transferDetails[0] = IAllowanceHolder.TransferDetails({token: TOKEN, recipient: RECIPIENT, amount: AMOUNT}); + ah.setAllowed(OPERATOR, OWNER, TOKEN, AMOUNT); vm.expectRevert(); ah.holderTransferFrom(OWNER, transferDetails); } function testPermitUnauthorisedAmount() public { - address token = address(new FallbackDummy()); - address operator = address(this); - - ah.setAllowed(operator, OWNER, token, AMOUNT); IAllowanceHolder.TransferDetails[] memory transferDetails = new IAllowanceHolder.TransferDetails[](1); - transferDetails[0] = IAllowanceHolder.TransferDetails({token: token, recipient: RECIPIENT, amount: AMOUNT + 1}); + transferDetails[0] = IAllowanceHolder.TransferDetails({token: TOKEN, recipient: RECIPIENT, amount: AMOUNT + 1}); + ah.setAllowed(OPERATOR, OWNER, TOKEN, AMOUNT); vm.expectRevert(); + vm.prank(OPERATOR); ah.holderTransferFrom(OWNER, transferDetails); } function testPermitUnauthorisedToken() public { - address token = address(new FallbackDummy()); - address operator = address(this); - - ah.setAllowed(operator, OWNER, token, AMOUNT); IAllowanceHolder.TransferDetails[] memory transferDetails = new IAllowanceHolder.TransferDetails[](1); transferDetails[0] = IAllowanceHolder.TransferDetails({token: TOKEN, recipient: RECIPIENT, amount: AMOUNT}); + ah.setAllowed(OPERATOR, OWNER, address(0xdead), AMOUNT); vm.expectRevert(); ah.holderTransferFrom(OWNER, transferDetails); } @@ -121,47 +133,15 @@ contract AllowanceHolderUnitTest is Test { } function testPermitExecute() public { - address token = address(new FallbackDummy()); - address target = address(new FallbackDummy()); + address target = _createNamedRejectionDummy("TARGET"); address operator = target; uint256 value = 999; ISignatureTransfer.TokenPermissions[] memory permits = new ISignatureTransfer.TokenPermissions[](1); - permits[0] = ISignatureTransfer.TokenPermissions({token: token, amount: AMOUNT}); + permits[0] = ISignatureTransfer.TokenPermissions({token: TOKEN, amount: AMOUNT}); bytes memory data = hex"deadbeef"; - vm.startStateDiffRecording(); + _mockExpectCall(address(target), abi.encodePacked(data, address(this)), abi.encode(true)); ah.execute{value: value}(operator, permits, payable(target), data); - VmSafe.AccountAccess[] memory calls = - _foundry_filterAccessKind(vm.stopAndReturnStateDiff(), VmSafe.AccountAccessKind.Call); - - // First Call is to AllowanceHolder with the `execute` calldata - // Second Call is to the Target with the `data` - // We test that the msg.sender is passed along appended to `data` - assertEq(calls[1].account, target); - assertEq(calls[1].data, abi.encodePacked(data, address(this))); - assertEq(calls[1].value, value); - } - - /// @dev Utility to filter the AccountAccess[] to just the particular kind we want - function _foundry_filterAccessKind(VmSafe.AccountAccess[] memory accesses, VmSafe.AccountAccessKind kind) - public - pure - returns (VmSafe.AccountAccess[] memory filtered) - { - filtered = new VmSafe.AccountAccess[](accesses.length); - uint256 count = 0; - - for (uint256 i = 0; i < accesses.length; i++) { - if (accesses[i].kind == kind) { - filtered[count] = accesses[i]; - count++; - } - } - - assembly { - // Resize the array - mstore(filtered, count) - } } } diff --git a/test/unit/Utils.sol b/test/unit/Utils.sol new file mode 100644 index 000000000..71d7652a4 --- /dev/null +++ b/test/unit/Utils.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {Test} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; + +contract RejectionFallbackDummy { + fallback() external payable { + revert("Rejected"); + } +} + +contract Utils { + Vm internal constant _vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + function _deterministicAddress(string memory name) internal returns (address a) { + a = address(bytes20(keccak256(abi.encodePacked(name)))); + _vm.label(a, name); + } + + function _createNamedRejectionDummy(string memory name) internal returns (address a) { + a = address(new RejectionFallbackDummy()); + _vm.label(a, name); + } + + function _etchNamedRejectionDummy(string memory name, address a) internal returns (address) { + _vm.etch(a, type(RejectionFallbackDummy).runtimeCode); + _vm.label(a, name); + return a; + } + + function _mockExpectCall(address callee, bytes memory data, bytes memory returnData) internal { + _vm.mockCall(callee, data, returnData); + _vm.expectCall(callee, data); + } + + function _mockExpectCall(address callee, uint256 value, bytes memory data, bytes memory returnData) internal { + _vm.mockCall(callee, value, data, returnData); + _vm.expectCall(callee, value, data); + } +} diff --git a/test/unit/core/BasicUnitTest.t.sol b/test/unit/core/BasicUnitTest.t.sol new file mode 100644 index 000000000..10df59eb0 --- /dev/null +++ b/test/unit/core/BasicUnitTest.t.sol @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {Basic} from "../../../src/core/Basic.sol"; +import {Permit2Payment} from "../../../src/core/Permit2Payment.sol"; + +import {IERC20} from "../../../src/IERC20.sol"; +import {Utils} from "../Utils.sol"; + +import {Test} from "forge-std/Test.sol"; + +contract BasicDummy is Basic, Permit2Payment { + constructor(address permit2, address feeRecipient, address allowanceHolder) + Permit2Payment(permit2, feeRecipient, allowanceHolder) + {} + + function sellToPool(address pool, IERC20 sellToken, uint256 bips, uint256 offset, bytes memory data) public { + super.basicSellToPool(pool, sellToken, bips, offset, data); + } +} + +contract BasicUnitTest is Utils, Test { + BasicDummy basic; + address PERMIT2 = _deterministicAddress("PERMIT2"); + address FEE_RECIPIENT = _deterministicAddress("FEE_RECIPIENT"); + address ALLOWANCE_HOLDER = _deterministicAddress("ALLOWANCE_HOLDER"); + address POOL = _createNamedRejectionDummy("POOL"); + IERC20 TOKEN = IERC20(_createNamedRejectionDummy("TOKEN")); + + function setUp() public { + basic = new BasicDummy(PERMIT2, FEE_RECIPIENT, ALLOWANCE_HOLDER); + } + + function testBasicSell() public { + uint256 bips = 10_000; + uint256 offset = 4; + uint256 amount = 99999; + bytes4 selector = bytes4(hex"12345678"); + bytes memory data = abi.encodePacked(selector, amount); + + _mockExpectCall( + address(TOKEN), abi.encodeWithSelector(IERC20.balanceOf.selector, address(basic)), abi.encode(amount) + ); + _mockExpectCall( + address(TOKEN), + abi.encodeWithSelector(IERC20.allowance.selector, address(basic), address(POOL)), + abi.encode(amount) + ); + + _mockExpectCall(address(POOL), data, abi.encode(true)); + + basic.sellToPool(POOL, TOKEN, bips, offset, data); + } + + /// @dev adjust the balange of the contract to be less than expected + function testBasicSellLowerBalanceAmount() public { + uint256 bips = 10_000; + uint256 offset = 4; + uint256 amount = 99999; + bytes4 selector = bytes4(hex"12345678"); + bytes memory data = abi.encodePacked(selector, amount); + + _mockExpectCall( + address(TOKEN), abi.encodeWithSelector(IERC20.balanceOf.selector, address(basic)), abi.encode(amount / 2) + ); + _mockExpectCall( + address(TOKEN), + abi.encodeWithSelector(IERC20.allowance.selector, address(basic), address(POOL)), + abi.encode(amount) + ); + + _mockExpectCall(address(POOL), abi.encodePacked(selector, amount / 2), abi.encode(true)); + basic.sellToPool(POOL, TOKEN, bips, offset, data); + } + + /// @dev adjust the balange of the contract to be greater than expected + function testBasicSellGreaterBalanceAmount() public { + uint256 bips = 10_000; + uint256 offset = 4; + uint256 amount = 99999; + bytes4 selector = bytes4(hex"12345678"); + bytes memory data = abi.encodePacked(selector, amount); + + _mockExpectCall( + address(TOKEN), abi.encodeWithSelector(IERC20.balanceOf.selector, address(basic)), abi.encode(amount * 2) + ); + _mockExpectCall( + address(TOKEN), + abi.encodeWithSelector(IERC20.allowance.selector, address(basic), address(POOL)), + abi.encode(amount * 2) + ); + + _mockExpectCall(address(POOL), abi.encodePacked(selector, amount * 2), abi.encode(true)); + basic.sellToPool(POOL, TOKEN, bips, offset, data); + } + + /// @dev When 0xeeee (native asset) is used we expect it to transfer as value + function testBasicSellEthValue() public { + uint256 bips = 10_000; + uint256 offset = 4; + uint256 amount = 99999; + uint256 value = amount; + bytes4 selector = bytes4(hex"12345678"); + bytes memory data = abi.encodePacked(selector, amount); + + _mockExpectCall(address(POOL), value, abi.encodePacked(selector, amount), abi.encode(true)); + + vm.deal(address(basic), value); + basic.sellToPool(POOL, IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), bips, offset, data); + } + + /// @dev When 0xeeee (native asset) is used we expect it to transfer as value and adjust for the current balance if lower + function testBasicSellLowerEthValue() public { + uint256 bips = 10_000; + uint256 offset = 4; + uint256 amount = 99999; + uint256 value = amount / 2; + bytes4 selector = bytes4(hex"12345678"); + bytes memory data = abi.encodePacked(selector, amount); + + _mockExpectCall(address(POOL), value, abi.encodePacked(selector, value), abi.encode(true)); + + vm.deal(address(basic), value); + basic.sellToPool(POOL, IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), bips, offset, data); + } + + /// @dev When 0xeeee (native asset) is used we expect it to transfer as value and adjust for the current balance if greater + function testBasicSellGreaterEthValue() public { + uint256 bips = 10_000; + uint256 offset = 4; + uint256 amount = 99999; + uint256 value = amount * 2; + bytes4 selector = bytes4(hex"12345678"); + bytes memory data = abi.encodePacked(selector, amount); + + _mockExpectCall(address(POOL), value, abi.encodePacked(selector, value), abi.encode(true)); + + vm.deal(address(basic), value); + basic.sellToPool(POOL, IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), bips, offset, data); + } + + /// @dev When 0xeeee (native asset) is used we expect it to transfer as value and adjust for the current balance + function testBasicSellAdjustedEthValue() public { + uint256 bips = 5_000; // sell half + uint256 offset = 4; + uint256 amount = 99999; + uint256 value = amount * 2; + bytes4 selector = bytes4(hex"12345678"); + bytes memory data = abi.encodePacked(selector, amount); + + // 5_000 / 10_000 * value == amount + _mockExpectCall(address(POOL), amount, abi.encodePacked(selector, amount), abi.encode(true)); + + vm.deal(address(basic), value); + basic.sellToPool(POOL, IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), bips, offset, data); + } + + /// @dev When 0xeeee (native asset) is used we expect it to support a transfer with no data + function testBasicSellTransferValue() public { + uint256 bips = 10_000; + uint256 offset = 0; + uint256 amount = 99999; + uint256 value = amount; + bytes memory data; + + _mockExpectCall(address(POOL), value, data, abi.encode(true)); + + vm.deal(address(basic), value); + basic.sellToPool(POOL, IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), bips, offset, data); + } + + function testBasicRestrictedTarget() public { + uint256 bips = 10_000; + uint256 offset = 0; + bytes memory data; + + vm.expectRevert(); + basic.sellToPool(PERMIT2, IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), bips, offset, data); + + vm.expectRevert(); + basic.sellToPool(ALLOWANCE_HOLDER, IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), bips, offset, data); + } + + function testBasicBubblesUpRevert() public { + uint256 bips = 10_000; + uint256 offset = 0; + bytes memory data; + + vm.expectRevert(); + basic.sellToPool(POOL, IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), bips, offset, data); + } +} diff --git a/test/unit/core/MakerPSMUnitTest.t.sol b/test/unit/core/MakerPSMUnitTest.t.sol new file mode 100644 index 000000000..dee665b32 --- /dev/null +++ b/test/unit/core/MakerPSMUnitTest.t.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {MakerPSM, IPSM} from "../../../src/core/MakerPSM.sol"; + +import {IERC20, IERC20Meta} from "../../../src/IERC20.sol"; +import {Utils} from "../Utils.sol"; + +import {Test} from "forge-std/Test.sol"; + +contract MakerPSMDummy is MakerPSM { + constructor(address dai) MakerPSM(dai) {} + + function sellToPool(address recipient, uint256 bips, address psm, address gemToken) public { + super.makerPsmSellGem(recipient, bips, IPSM(psm), IERC20Meta(gemToken)); + } + + function buyFromPool(address recipient, uint256 bips, address psm, address gemToken) public { + super.makerPsmBuyGem(recipient, bips, IPSM(psm), IERC20Meta(gemToken)); + } +} + +contract MakerPSMUnitTest is Utils, Test { + MakerPSMDummy psm; + address POOL = _createNamedRejectionDummy("POOL"); + address RECIPIENT = _createNamedRejectionDummy("RECIPIENT"); + address PSM = _createNamedRejectionDummy("PSM"); + address DAI = _createNamedRejectionDummy("DAI"); + address TOKEN = _createNamedRejectionDummy("TOKEN"); + + function setUp() public { + psm = new MakerPSMDummy(DAI); + } + + function testMakerPSMSell() public { + uint256 bips = 10_000; + uint256 amount = 99999; + + _mockExpectCall(TOKEN, abi.encodeWithSelector(IERC20.balanceOf.selector, address(psm)), abi.encode(amount)); + _mockExpectCall(TOKEN, abi.encodeWithSelector(IERC20.allowance.selector, address(psm), PSM), abi.encode(amount)); + _mockExpectCall(PSM, abi.encodeWithSelector(IPSM.gemJoin.selector), abi.encode(PSM)); + _mockExpectCall(PSM, abi.encodeWithSelector(IPSM.sellGem.selector, RECIPIENT, amount), abi.encode(true)); + + psm.sellToPool(RECIPIENT, bips, PSM, TOKEN); + } + + function testMakerPSMBuy() public { + uint256 bips = 10_000; + uint256 amount = 99999; + + _mockExpectCall(DAI, abi.encodeWithSelector(IERC20.balanceOf.selector, address(psm)), abi.encode(amount)); + _mockExpectCall(DAI, abi.encodeWithSelector(IERC20.allowance.selector, address(psm), PSM), abi.encode(amount)); + _mockExpectCall(TOKEN, abi.encodeWithSelector(IERC20Meta.decimals.selector), abi.encode(18)); + _mockExpectCall(PSM, abi.encodeWithSelector(IPSM.tout.selector), abi.encode(100)); + _mockExpectCall(PSM, abi.encodeWithSelector(IPSM.buyGem.selector, RECIPIENT, 99998), abi.encode(true)); + + psm.buyFromPool(RECIPIENT, bips, PSM, TOKEN); + } +} diff --git a/test/unit/core/OtcUnitTest.t.sol b/test/unit/core/OtcUnitTest.t.sol new file mode 100644 index 000000000..4a4ec1c39 --- /dev/null +++ b/test/unit/core/OtcUnitTest.t.sol @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {OtcOrderSettlement} from "../../../src/core/OtcOrderSettlement.sol"; +import {Permit2Payment} from "../../../src/core/Permit2Payment.sol"; +import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol"; +import {IAllowanceHolder} from "../../../src/IAllowanceHolder.sol"; + +import {Utils} from "../Utils.sol"; +import {IERC20} from "../../../src/IERC20.sol"; + +import {Test} from "forge-std/Test.sol"; + +contract OtcOrderSettlementDummy is OtcOrderSettlement, Permit2Payment { + constructor(address permit2, address feeRecipient, address allowanceHolder) + Permit2Payment(permit2, feeRecipient, allowanceHolder) + {} + + function considerationWitnessType() external pure returns (string memory) { + return CONSIDERATION_WITNESS; + } + + function actionsAndSlippageWitnessType() external pure returns (string memory) { + return ACTIONS_AND_SLIPPAGE_WITNESS; + } + + function fillOtcOrderDirectCounterparties( + address recipient, + ISignatureTransfer.PermitTransferFrom memory makerPermit, + address maker, + bytes memory makerSig, + ISignatureTransfer.PermitTransferFrom memory takerPermit, + bytes memory takerSig + ) external { + super.fillOtcOrder(recipient, makerPermit, maker, makerSig, takerPermit, takerSig); + } + + function fillOtcOrderSelf( + address recipient, + ISignatureTransfer.PermitTransferFrom memory permit, + address maker, + bytes memory makerSig, + address takerToken, + uint256 maxTakerAmount, + address msgSender + ) external { + super.fillOtcOrderSelfFunded(recipient, permit, maker, makerSig, IERC20(takerToken), maxTakerAmount, msgSender); + } + + function fillOtcOrderMeta( + address recipient, + ISignatureTransfer.PermitTransferFrom memory makerPermit, + address maker, + bytes memory makerSig, + ISignatureTransfer.PermitTransferFrom memory takerPermit, + address taker, + bytes memory takerSig, + bytes32 takerWitness + ) external { + super.fillOtcOrderMetaTxn(recipient, makerPermit, maker, makerSig, takerPermit, taker, takerSig, takerWitness); + } +} + +contract OtcUnitTest is Utils, Test { + OtcOrderSettlementDummy otc; + address PERMIT2 = _createNamedRejectionDummy("PERMIT2"); + address FEE_RECIPIENT = _createNamedRejectionDummy("FEE_RECIPIENT"); + address ALLOWANCE_HOLDER = _createNamedRejectionDummy("ALLOWANCE_HOLDER"); + + address TOKEN0 = _createNamedRejectionDummy("TOKEN0"); + address TOKEN1 = _createNamedRejectionDummy("TOKEN1"); + address RECIPIENT = _createNamedRejectionDummy("RECIPIENT"); + address MAKER = _createNamedRejectionDummy("MAKER"); + + function setUp() public { + otc = new OtcOrderSettlementDummy(PERMIT2, FEE_RECIPIENT, ALLOWANCE_HOLDER); + } + + function testOtcDirectCounterparties() public { + // 🎉 + uint256 amount = 9999; + ISignatureTransfer.PermitTransferFrom memory makerPermit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: TOKEN1, amount: amount}), + nonce: 0, + deadline: 0 + }); + ISignatureTransfer.PermitTransferFrom memory takerPermit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: TOKEN0, amount: amount}), + nonce: 0, + deadline: 0 + }); + + _mockExpectCall( + PERMIT2, + abi.encodeWithSelector( + bytes4(0x137c29fe), + makerPermit, + ISignatureTransfer.SignatureTransferDetails({to: RECIPIENT, requestedAmount: amount}), + MAKER, + bytes32(0x315954c1f9717c9d14604de3c6ceb9fd601b3bd1d0b8ec397e8c2b81668a02e1), /* witness */ + otc.considerationWitnessType(), + hex"dead" + ), + new bytes(0) + ); + + _mockExpectCall( + PERMIT2, + abi.encodeWithSelector( + bytes4(0x30f28b7a), + takerPermit, + ISignatureTransfer.SignatureTransferDetails({to: MAKER, requestedAmount: amount}), + address(this), /* taker + payer */ + hex"beef" + ), + new bytes(0) + ); + + // Broken usage of OtcOrderSettlement.OtcOrderFilled in 0.8.21 + // https://github.com/foundry-rs/foundry/issues/6206 + // vm.expectEmit(address(otc)); + // emit OtcOrderSettlement.OtcOrderFilled( + // bytes32(0xbee0e2de3e64ecfe06fe7118215a033ac40a8d6a508d60b81cd9ac6addd6e11e), + // MAKER, + // address(this), + // TOKEN1, + // TOKEN0, + // amount, + // amount + // ); + + otc.fillOtcOrderDirectCounterparties(RECIPIENT, makerPermit, MAKER, hex"dead", takerPermit, hex"beef"); + } + + function testOtcDirectCounterpartiesViaAllowanceHolder() public { + uint256 amount = 9999; + ISignatureTransfer.PermitTransferFrom memory makerPermit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: TOKEN1, amount: amount}), + nonce: 0, + deadline: 0 + }); + ISignatureTransfer.PermitTransferFrom memory takerPermit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: TOKEN0, amount: amount}), + nonce: 0, + deadline: 0 + }); + + _mockExpectCall( + PERMIT2, + abi.encodeWithSelector( + bytes4(0x137c29fe), + makerPermit, + ISignatureTransfer.SignatureTransferDetails({to: RECIPIENT, requestedAmount: amount}), + MAKER, + bytes32(0x315954c1f9717c9d14604de3c6ceb9fd601b3bd1d0b8ec397e8c2b81668a02e1), /* witness */ + otc.considerationWitnessType(), + hex"dead" + ), + new bytes(0) + ); + + IAllowanceHolder.TransferDetails[] memory transferDetails = new IAllowanceHolder.TransferDetails[](1); + transferDetails[0] = IAllowanceHolder.TransferDetails({token: TOKEN0, recipient: MAKER, amount: amount}); + + _mockExpectCall( + ALLOWANCE_HOLDER, + abi.encodeCall(IAllowanceHolder.holderTransferFrom, (address(this), transferDetails)), + abi.encode(true) + ); + + // Broken usage of OtcOrderSettlement.OtcOrderFilled in 0.8.21 + // https://github.com/foundry-rs/foundry/issues/6206 + // vm.expectEmit(address(otc)); + // emit OtcOrderSettlement.OtcOrderFilled( + // bytes32(0xbee0e2de3e64ecfe06fe7118215a033ac40a8d6a508d60b81cd9ac6addd6e11e), + // MAKER, + // address(this), + // TOKEN1, + // TOKEN0, + // amount, + // amount + // ); + + vm.prank(ALLOWANCE_HOLDER); + (bool success,) = address(otc).call( + abi.encodePacked( + abi.encodeCall( + otc.fillOtcOrderDirectCounterparties, (RECIPIENT, makerPermit, MAKER, hex"dead", takerPermit, hex"") + ), + address(this) + ) // Forward on true msg.sender + ); + require(success); + } + + function testOtcSelfFunded() public { + uint256 amount = 9999; + ISignatureTransfer.PermitTransferFrom memory makerPermit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: TOKEN1, amount: amount}), + nonce: 0, + deadline: 0 + }); + + _mockExpectCall( + PERMIT2, + abi.encodeWithSelector( + bytes4(0x137c29fe), + makerPermit, + ISignatureTransfer.SignatureTransferDetails({to: RECIPIENT, requestedAmount: amount}), + MAKER, + bytes32(0x30fd0fb242892788e98ff4323f8e366b5f1dd9a4c033f5ea6ae4252f6e887e37), /* witness */ + "Consideration consideration)Consideration(address token,uint256 amount,address counterparty,bool partialFillAllowed)TokenPermissions(address token,uint256 amount)", + hex"dead" + ), + new bytes(0) + ); + + _mockExpectCall( + address(TOKEN0), abi.encodeWithSelector(IERC20.balanceOf.selector, address(otc)), abi.encode(amount) + ); + + _mockExpectCall( + address(TOKEN0), abi.encodeWithSelector(IERC20.transfer.selector, MAKER, amount), abi.encode(true) + ); + + // Broken usage of OtcOrderSettlement.OtcOrderFilled in 0.8.21 + // https://github.com/foundry-rs/foundry/issues/6206 + // vm.expectEmit(address(otc)); + // emit OtcOrderSettlement.OtcOrderFilled( + // bytes32(0x33d473fdc5cd07e2f752b882bb4f51ccc88c742aa085ebdcbd4af689aba7ffd4), + // MAKER, + // address(this), + // TOKEN1, + // TOKEN0, + // amount, + // amount + // ); + + otc.fillOtcOrderSelf(RECIPIENT, makerPermit, MAKER, hex"dead", TOKEN0, amount, address(this)); + } + + function testOtcMetaTxn() public { + uint256 amount = 9999; + ISignatureTransfer.PermitTransferFrom memory makerPermit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: TOKEN1, amount: amount}), + nonce: 0, + deadline: 0 + }); + ISignatureTransfer.PermitTransferFrom memory takerPermit = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: TOKEN0, amount: amount}), + nonce: 0, + deadline: 0 + }); + + // Maker payment via Permit2 + _mockExpectCall( + PERMIT2, + abi.encodeWithSelector( + bytes4(0x137c29fe), + makerPermit, + ISignatureTransfer.SignatureTransferDetails({to: RECIPIENT, requestedAmount: amount}), + MAKER, + bytes32(0x315954c1f9717c9d14604de3c6ceb9fd601b3bd1d0b8ec397e8c2b81668a02e1), /* witness */ + otc.considerationWitnessType(), + hex"dead" + ), + new bytes(0) + ); + + // Taker payment via Permit2 + _mockExpectCall( + PERMIT2, + abi.encodeWithSelector( + bytes4(0x137c29fe), + takerPermit, + ISignatureTransfer.SignatureTransferDetails({to: MAKER, requestedAmount: amount}), + address(this), /* taker */ + bytes32(0x0000000000000000000000000000000000000000000000000000000000000000), /* witness */ + otc.actionsAndSlippageWitnessType(), + hex"beef" + ), + new bytes(0) + ); + + // Broken usage of OtcOrderSettlement.OtcOrderFilled in 0.8.21 + // https://github.com/foundry-rs/foundry/issues/6206 + // vm.expectEmit(address(otc)); + // emit OtcOrderSettlement.OtcOrderFilled( + // bytes32(0xbee0e2de3e64ecfe06fe7118215a033ac40a8d6a508d60b81cd9ac6addd6e11e), + // MAKER, + // address(this), + // TOKEN1, + // TOKEN0, + // amount, + // amount + // ); + + otc.fillOtcOrderMeta( + RECIPIENT, makerPermit, MAKER, hex"dead", takerPermit, address(this), hex"beef", bytes32(0x00) + ); + } +} diff --git a/test/unit/core/UniswapV2UnitTest.t.sol b/test/unit/core/UniswapV2UnitTest.t.sol new file mode 100644 index 000000000..6fec93e08 --- /dev/null +++ b/test/unit/core/UniswapV2UnitTest.t.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {UniswapV2} from "../../../src/core/UniswapV2.sol"; + +import {Utils} from "../Utils.sol"; +import {IERC20} from "../../../src/IERC20.sol"; + +import {Test} from "forge-std/Test.sol"; + +contract UniswapV2Dummy is UniswapV2 { + function sell(address recipient, bytes memory encodedPath, uint256 bips, uint256 minBuyAmount) public { + super.sellToUniswapV2(recipient, encodedPath, bips, minBuyAmount); + } +} + +contract UniswapV2UnitTest is Utils, Test { + UniswapV2Dummy uni; + address TOKEN0 = _createNamedRejectionDummy("TOKEN0"); + address TOKEN1 = _createNamedRejectionDummy("TOKEN1"); + address TOKEN2 = _createNamedRejectionDummy("TOKEN2"); + address POOL = _etchNamedRejectionDummy("POOL", 0xabedA74b789DBa7D817889Eb0266E1F58219f13f); // created from TOKEN0/TOKEN1 combo + address POOL2 = _etchNamedRejectionDummy("POOL2", 0x62D5437A22Ab167ABbe5e2FADe8C49bE7276ab2F); // created from TOKEN1/TOKEN2 combo + address RECIPIENT = _createNamedRejectionDummy("RECIPIENT"); + + function setUp() public { + uni = new UniswapV2Dummy(); + } + + function testUniswapV2Sell() public { + uint256 bips = 10_000; + uint256 amount = 99999; + uint256 minBuyAmount = 9087; + + _mockExpectCall(TOKEN0, abi.encodeWithSelector(IERC20.balanceOf.selector, address(uni)), abi.encode(amount)); + _mockExpectCall(TOKEN0, abi.encodeWithSelector(IERC20.transfer.selector, POOL, amount), new bytes(0)); + + // UniswapV2Pool.getReserves + _mockExpectCall(POOL, abi.encodePacked(bytes4(0x0902f1ac)), abi.encode(uint256(9999), uint256(9999))); + // UniswapV2Pool.swap + _mockExpectCall( + POOL, + abi.encodePacked(bytes4(0x022c0d9f), abi.encode(uint256(9087), 0, RECIPIENT, new bytes(0))), + new bytes(0) + ); + + uni.sell(RECIPIENT, abi.encodePacked(TOKEN0, uint8(1), TOKEN1), bips, minBuyAmount); + } + + function testUniswapV2SellSlippageCheck() public { + uint256 bips = 10_000; + uint256 amount = 99999; + uint256 minBuyAmount = 1e18; + + _mockExpectCall(TOKEN0, abi.encodeWithSelector(IERC20.balanceOf.selector, address(uni)), abi.encode(amount)); + _mockExpectCall(TOKEN0, abi.encodeWithSelector(IERC20.transfer.selector, POOL, amount), new bytes(0)); + + // UniswapV2Pool.getReserves + _mockExpectCall(POOL, abi.encodePacked(bytes4(0x0902f1ac)), abi.encode(uint256(9999), uint256(9999))); + // UniswapV2Pool.swap + _mockExpectCall( + POOL, + abi.encodePacked(bytes4(0x022c0d9f), abi.encode(uint256(9087), 0, RECIPIENT, new bytes(0))), + new bytes(0) + ); + + vm.expectRevert(); + uni.sell(RECIPIENT, abi.encodePacked(TOKEN0, uint8(1), TOKEN1), bips, minBuyAmount); + } + + function testUniswapV2LowerAmount() public { + uint256 bips = 10_000; + uint256 amount = 99999; + uint256 minBuyAmount = 1; + + _mockExpectCall(TOKEN0, abi.encodeWithSelector(IERC20.balanceOf.selector, address(uni)), abi.encode(amount / 2)); + _mockExpectCall(TOKEN0, abi.encodeWithSelector(IERC20.transfer.selector, POOL, amount / 2), new bytes(0)); + + // UniswapV2Pool.getReserves + _mockExpectCall(POOL, abi.encodePacked(bytes4(0x0902f1ac)), abi.encode(uint256(9999), uint256(9999))); + // UniswapV2Pool.swap + _mockExpectCall( + POOL, + abi.encodePacked(bytes4(0x022c0d9f), abi.encode(uint256(8328), 0, RECIPIENT, new bytes(0))), + new bytes(0) + ); + + uni.sell(RECIPIENT, abi.encodePacked(TOKEN0, uint8(1), TOKEN1), bips, minBuyAmount); + } + + function testUniswapV2GreaterAmount() public { + uint256 bips = 10_000; + uint256 amount = 99999; + uint256 minBuyAmount = 9521; + + _mockExpectCall(TOKEN0, abi.encodeWithSelector(IERC20.balanceOf.selector, address(uni)), abi.encode(amount * 2)); + _mockExpectCall(TOKEN0, abi.encodeWithSelector(IERC20.transfer.selector, POOL, amount * 2), new bytes(0)); + + // UniswapV2Pool.getReserves + _mockExpectCall(POOL, abi.encodePacked(bytes4(0x0902f1ac)), abi.encode(uint256(9999), uint256(9999))); + // UniswapV2Pool.swap + _mockExpectCall( + POOL, + abi.encodePacked(bytes4(0x022c0d9f), abi.encode(uint256(9521), 0, RECIPIENT, new bytes(0))), + new bytes(0) + ); + + uni.sell(RECIPIENT, abi.encodePacked(TOKEN0, uint8(1), TOKEN1), bips, minBuyAmount); + } + + function testUniswapV2SellTokenFee() public { + uint256 bips = 10_000; + uint256 amount = 99999; + uint256 minBuyAmount = 1; + + // Sell token fee branch is selected if the hopInfo param has the first bit flipped to 1 + uint8 hopInfo = uint8(1) | 0x80; + // We emulate a token which has a 50% fee when transferring to the Uniswap pool + _mockExpectCall(TOKEN0, abi.encodeWithSelector(IERC20.balanceOf.selector, POOL), abi.encode(amount / 2)); + + _mockExpectCall(TOKEN0, abi.encodeWithSelector(IERC20.balanceOf.selector, address(uni)), abi.encode(amount)); + _mockExpectCall(TOKEN0, abi.encodeWithSelector(IERC20.transfer.selector, POOL, amount), new bytes(0)); + + // UniswapV2Pool.getReserves + _mockExpectCall(POOL, abi.encodePacked(bytes4(0x0902f1ac)), abi.encode(uint256(9999), uint256(9999))); + // UniswapV2Pool.swap + _mockExpectCall( + POOL, + abi.encodePacked(bytes4(0x022c0d9f), abi.encode(uint256(7994), 0, RECIPIENT, new bytes(0))), + new bytes(0) + ); + // the pool is responsible for transferring to receipient, since the pool is a dummy, this transfer is not mocked + + uni.sell(RECIPIENT, abi.encodePacked(TOKEN0, hopInfo, TOKEN1), bips, minBuyAmount); + } + + function testUniswapV2Multihop() public { + uint256 bips = 10_000; + uint256 amount = 99999; + uint256 minBuyAmount = 4869; + + _mockExpectCall(TOKEN0, abi.encodeWithSelector(IERC20.balanceOf.selector, address(uni)), abi.encode(amount * 2)); + _mockExpectCall(TOKEN0, abi.encodeWithSelector(IERC20.transfer.selector, POOL, amount * 2), new bytes(0)); + + // UniswapV2Pool.getReserves + _mockExpectCall(POOL, abi.encodePacked(bytes4(0x0902f1ac)), abi.encode(uint256(9999), uint256(9999))); + // UniswapV2Pool.swap + // POOL specifies POOL2 as recipient + _mockExpectCall( + POOL, abi.encodePacked(bytes4(0x022c0d9f), abi.encode(uint256(9521), 0, POOL2, new bytes(0))), new bytes(0) + ); + _mockExpectCall(POOL2, abi.encodePacked(bytes4(0x0902f1ac)), abi.encode(uint256(9999), uint256(9999))); + // UniswapV2Pool.swap + // POOL2 specifies RECIPIENT as recipient + _mockExpectCall( + POOL2, + abi.encodePacked(bytes4(0x022c0d9f), abi.encode(uint256(0), uint256(4869), RECIPIENT, new bytes(0))), + new bytes(0) + ); + + uni.sell(RECIPIENT, abi.encodePacked(TOKEN0, uint8(1), TOKEN1, uint8(1), TOKEN2), bips, minBuyAmount); + } +} diff --git a/test/unit/core/UniswapV3UnitTest.t.sol b/test/unit/core/UniswapV3UnitTest.t.sol new file mode 100644 index 000000000..31c75053c --- /dev/null +++ b/test/unit/core/UniswapV3UnitTest.t.sol @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {UniswapV3, IUniswapV3Pool} from "../../../src/core/UniswapV3.sol"; +import {Permit2Payment} from "../../../src/core/Permit2Payment.sol"; +import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol"; + +import {IAllowanceHolder} from "../../../src/IAllowanceHolder.sol"; + +import {Utils} from "../Utils.sol"; +import {IERC20} from "../../../src/IERC20.sol"; + +import {Test} from "forge-std/Test.sol"; + +contract UniswapV3Dummy is UniswapV3, Permit2Payment { + constructor(address uniFactory, bytes32 poolInit, address permit2, address feeRecipient, address allowanceHolder) + UniswapV3(uniFactory, poolInit) + Permit2Payment(permit2, feeRecipient, allowanceHolder) + {} + + function sellTokenForTokenSelf(address recipient, bytes memory encodedPath, uint256 bips, uint256 minBuyAmount) + external + returns (uint256) + { + return super.sellTokenForTokenToUniswapV3(recipient, encodedPath, bips, minBuyAmount); + } + + function sellTokenForToken( + address recipient, + bytes memory encodedPath, + uint256 sellAmount, + uint256 minBuyAmount, + address payer, + ISignatureTransfer.PermitTransferFrom memory permit, + bytes memory sig + ) external returns (uint256) { + return super.sellTokenForTokenToUniswapV3(recipient, encodedPath, sellAmount, minBuyAmount, payer, permit, sig); + } +} + +/// @dev We need a dummy to actually call our contract, so it needs an implementation which at the very least +/// calls the `uniswapV3SwapCallback` +contract UniswapV3PoolDummy { + bytes public RETURN_DATA; + + constructor(bytes memory returnData) { + RETURN_DATA = returnData; + } + + fallback(bytes calldata) external payable returns (bytes memory) { + (,,,, bytes memory data) = abi.decode(msg.data[4:], (address, bool, int256, uint160, bytes)); + msg.sender.call(abi.encodeWithSelector(UniswapV3.uniswapV3SwapCallback.selector, int256(1), int256(1), data)); + return RETURN_DATA; + } +} + +contract UniswapV3UnitTest is Utils, Test { + UniswapV3Dummy uni; + address UNI_FACTORY = _createNamedRejectionDummy("UNI_FACTORY"); + address PERMIT2 = _createNamedRejectionDummy("PERMIT2"); + address FEE_RECIPIENT = _createNamedRejectionDummy("FEE_RECIPIENT"); + address ALLOWANCE_HOLDER = _createNamedRejectionDummy("ALLOWANCE_HOLDER"); + + address TOKEN0 = _createNamedRejectionDummy("TOKEN0"); + address TOKEN1 = _createNamedRejectionDummy("TOKEN1"); + address TOKEN2 = _createNamedRejectionDummy("TOKEN2"); + address RECIPIENT = _createNamedRejectionDummy("RECIPIENT"); + + address POOL = _etchNamedRejectionDummy("POOL", 0x33da22E66cE9c37747B80804c14dCE4a5aBD33a5); // created from TOKEN0/TOKEN1 combo + + function setUp() public { + uni = new UniswapV3Dummy( + UNI_FACTORY, keccak256(abi.encodePacked("POOL_INIT")), PERMIT2, FEE_RECIPIENT, ALLOWANCE_HOLDER + ); + } + + function testUniswapV3SellSelfFunded() public { + uint256 bips = 10_000; + uint256 amount = 99999; + uint256 minBuyAmount = amount; + + bytes memory data = abi.encodePacked(TOKEN0, uint24(500), TOKEN1); + + _mockExpectCall(TOKEN0, abi.encodeWithSelector(IERC20.balanceOf.selector, address(uni)), abi.encode(amount)); + _mockExpectCall( + POOL, + abi.encodeWithSelector( + IUniswapV3Pool.swap.selector, + RECIPIENT, + false, + amount, + 1461446703485210103287273052203988822378723970341, + abi.encodePacked(TOKEN1, uint24(500), TOKEN0, address(uni)) /* token1 and token0 swapped due to univ3 ordering */ + ), + abi.encode(-int256(amount), 0) + ); + + uni.sellTokenForTokenSelf(RECIPIENT, data, bips, minBuyAmount); + } + + function testUniswapV3SellSlippage() public { + uint256 bips = 10_000; + uint256 amount = 99999; + uint256 minBuyAmount = amount + 1; + + bytes memory data = abi.encodePacked(TOKEN0, uint24(500), TOKEN1); + + _mockExpectCall(TOKEN0, abi.encodeWithSelector(IERC20.balanceOf.selector, address(uni)), abi.encode(amount)); + _mockExpectCall( + POOL, + abi.encodeWithSelector( + IUniswapV3Pool.swap.selector, + RECIPIENT, + false, + amount, + 1461446703485210103287273052203988822378723970341, + abi.encodePacked(TOKEN1, uint24(500), TOKEN0, address(uni)) /* token1 and token0 swapped due to univ3 ordering */ + ), + abi.encode(-int256(amount), 0) + ); + + vm.expectRevert(); + uni.sellTokenForTokenSelf(RECIPIENT, data, bips, minBuyAmount); + } + + function testUniswapV3SellPermit2() public { + uint256 amount = 99999; + uint256 minBuyAmount = amount; + + bytes memory data = abi.encodePacked(TOKEN0, uint24(500), TOKEN1); + // Override the UniswapV3 pool code to callback our contract + // There's probably a smarter way to do this tbh + deployCodeTo( + "UniswapV3UnitTest.t.sol:UniswapV3PoolDummy", + abi.encode(abi.encodePacked(-int256(amount), -int256(amount))), + POOL + ); + + ISignatureTransfer.TokenPermissions memory permitted = + ISignatureTransfer.TokenPermissions({token: TOKEN0, amount: amount}); + ISignatureTransfer.PermitTransferFrom memory permitTransfer = + ISignatureTransfer.PermitTransferFrom({permitted: permitted, nonce: 0, deadline: 0}); + ISignatureTransfer.SignatureTransferDetails memory transferDetails = + ISignatureTransfer.SignatureTransferDetails({to: POOL, requestedAmount: amount}); + + // permitTransferFrom(((address,uint256),uint256,uint256),(address,uint256),address,bytes) 30f28b7a + // cannot use abi.encodeWithSelector due to the selector overload and ambiguity + _mockExpectCall( + PERMIT2, + abi.encodeWithSelector(bytes4(0x30f28b7a), permitTransfer, transferDetails, address(this), hex"deadbeef"), + new bytes(0) + ); + + uni.sellTokenForToken(RECIPIENT, data, amount, minBuyAmount, address(this), permitTransfer, hex"deadbeef"); + } + + function testUniswapV3SellAllowanceHolder() public { + uint256 amount = 99999; + uint256 minBuyAmount = amount; + + bytes memory data = abi.encodePacked(TOKEN0, uint24(500), TOKEN1); + // Override the UniswapV3 pool code to callback our contract + // There's probably a smarter way to do this tbh + deployCodeTo( + "UniswapV3UnitTest.t.sol:UniswapV3PoolDummy", + abi.encode(abi.encodePacked(-int256(amount), -int256(amount))), + POOL + ); + + ISignatureTransfer.PermitTransferFrom memory permitTransfer = ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: TOKEN0, amount: amount}), + nonce: 0, + deadline: 0 + }); + + IAllowanceHolder.TransferDetails[] memory transferDetails = new IAllowanceHolder.TransferDetails[](1); + transferDetails[0] = IAllowanceHolder.TransferDetails({token: TOKEN0, recipient: POOL, amount: amount}); + + _mockExpectCall( + ALLOWANCE_HOLDER, + abi.encodeCall(IAllowanceHolder.holderTransferFrom, (address(this), transferDetails)), + abi.encode(true) + ); + + vm.prank(ALLOWANCE_HOLDER); + address(uni).call( + abi.encodePacked( + abi.encodeCall( + uni.sellTokenForToken, (RECIPIENT, data, amount, minBuyAmount, address(this), permitTransfer, hex"") + ), + address(this) + ) // Forward on true msg.sender + ); + // uni.sellTokenForToken(RECIPIENT, data, amount, minBuyAmount, address(this), permitTransfer, hex""); + } +}