Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unit tests to output rebate design #27

Closed
wants to merge 40 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
8f4a29d
Refactor tests and add arbitrary call
zhongeric Nov 30, 2023
7fa5bce
forge fmt
zhongeric Nov 30, 2023
23c460d
add comment
zhongeric Nov 30, 2023
a1ff78d
add comment
zhongeric Nov 30, 2023
f1d91ef
Remove output token and lib
zhongeric Nov 30, 2023
85b6976
Remove output tokens and update tests
zhongeric Dec 1, 2023
5ad6830
bubble up revert reason
zhongeric Dec 1, 2023
4180cc8
Remove unused currencyLibary
zhongeric Dec 1, 2023
2793258
Change local imports to remote and natspe
zhongeric Dec 1, 2023
3e47dfe
forge fmt
zhongeric Dec 1, 2023
2482fe9
Re-add errors
zhongeric Dec 1, 2023
1fbcf88
save state
zhongeric Dec 5, 2023
8be2901
Merge branch 'main' into feat-no-output-settle
zhongeric Dec 5, 2023
ee9423a
remove constructor param (#13)
zhongeric Dec 6, 2023
9f846b9
Initial types refactor for signed amounts
zhongeric Dec 6, 2023
127d625
Fix decay lib tests
zhongeric Dec 6, 2023
388c8c3
Fix out of bounds error and integ tests pass
zhongeric Dec 6, 2023
f152283
Add unit tests
zhongeric Dec 6, 2023
bc99032
Make RelayOrderReactor inherit IReactor
zhongeric Dec 6, 2023
343c294
Add comment
zhongeric Dec 6, 2023
a8815e6
natspec
zhongeric Dec 6, 2023
28a6d9b
Merge branch 'feat-reactor-callback' into feat-negative-fee
zhongeric Dec 6, 2023
b0eee1c
Merge branch 'feat-negative-fee' into feat-add-unit-tests
zhongeric Dec 11, 2023
c604a66
Add basic unit test framework
zhongeric Dec 11, 2023
c6154d7
Add unit test for negative fee
zhongeric Dec 11, 2023
27caedd
Merge branch 'main' into feat-negative-fee
zhongeric Dec 11, 2023
de96ef5
Remove unused func
zhongeric Dec 12, 2023
094f1bb
nit: comment
zhongeric Dec 12, 2023
874b4fa
Add unit tests but one broken
zhongeric Dec 15, 2023
d763340
update 712 hashing
zhongeric Dec 15, 2023
47a42fe
Fix 712 ordering
zhongeric Dec 15, 2023
527d931
Merge branch 'feat-negative-fee' into feat-add-unit-tests
zhongeric Dec 15, 2023
3cbb671
Fix 712 ordering
zhongeric Dec 15, 2023
5ab717f
Merge branch 'feat-negative-fee' into feat-add-unit-tests
zhongeric Dec 15, 2023
a69858d
gas snap
zhongeric Dec 18, 2023
2303bfc
forge fmt
zhongeric Dec 18, 2023
a8c95f2
vm.chainId(1)
zhongeric Dec 18, 2023
6cb32ba
Merge branch 'feat-negative-fee' into feat-add-unit-tests
zhongeric Dec 18, 2023
6eca9c4
permit maxAmount in tests
zhongeric Dec 18, 2023
00aaa09
warping timestamp is causing ecrecover to fail?
snreynolds Dec 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .forge-snapshots/ExecuteSingle.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
108435
1 change: 1 addition & 0 deletions .forge-snapshots/ExecuteSingleWithRebate.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
107639
Original file line number Diff line number Diff line change
@@ -1 +1 @@
254346
256240
Original file line number Diff line number Diff line change
@@ -1 +1 @@
280710
282618
Original file line number Diff line number Diff line change
@@ -1 +1 @@
310415
312304
13 changes: 10 additions & 3 deletions src/base/ReactorStructs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ import {OrderInfo, OutputToken} from "UniswapX/src/base/ReactorStructs.sol";
/// @dev tokens that need to be sent from the swapper in order to satisfy an order
struct InputTokenWithRecipient {
ERC20 token;
uint256 amount;
// Needed for dutch decaying inputs
uint256 maxAmount;
int256 amount;
int256 maxAmount;
address recipient;
}

/// @dev An amount of input tokens that increases linearly over time
struct RelayInput {
ERC20 token;
uint256 startAmount;
uint256 endAmount;
address recipient;
}

Expand Down
15 changes: 0 additions & 15 deletions src/interfaces/IRelayOrderReactor.sol

This file was deleted.

12 changes: 12 additions & 0 deletions src/interfaces/IRelayOrderReactorCallback.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import {ResolvedRelayOrder} from "../base/ReactorStructs.sol";

/// @notice Callback for executing orders through a reactor.
interface IRelayOrderReactorCallback {
/// @notice Called by the reactor after actions are exected in an order
/// @param resolvedOrders Has inputs and outputs
/// @param callbackData The callbackData specified for an order execution
function reactorCallback(ResolvedRelayOrder[] memory resolvedOrders, bytes memory callbackData) external;
}
66 changes: 30 additions & 36 deletions src/lib/Permit2Lib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,24 @@ pragma solidity ^0.8.0;
import {ERC20} from "solmate/src/tokens/ERC20.sol";
import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol";
import {ResolvedOrder} from "UniswapX/src/base/ReactorStructs.sol";
import {ResolvedRelayOrder} from "../base/ReactorStructs.sol";
import {ResolvedRelayOrder, InputTokenWithRecipient} from "../base/ReactorStructs.sol";

/// @notice handling some permit2-specific encoding
library Permit2Lib {
/// @notice returns a ResolvedOrder into a permit object
function toPermit(ResolvedOrder memory order)
internal
pure
returns (ISignatureTransfer.PermitTransferFrom memory)
{
return ISignatureTransfer.PermitTransferFrom({
permitted: ISignatureTransfer.TokenPermissions({
token: address(order.input.token),
amount: order.input.maxAmount
}),
nonce: order.info.nonce,
deadline: order.info.deadline
});
}

/// @notice returns a ResolvedOrder into a permit object
function transferDetails(ResolvedOrder memory order, address to)
internal
pure
returns (ISignatureTransfer.SignatureTransferDetails memory)
{
return ISignatureTransfer.SignatureTransferDetails({to: to, requestedAmount: order.input.amount});
}

/// @notice returns a ResolvedOrder into a permit object
function toPermit(ResolvedRelayOrder memory order)
internal
pure
view
returns (ISignatureTransfer.PermitBatchTransferFrom memory)
{
InputTokenWithRecipient[] memory inputsRequired = getPositiveInputs(order.inputs);
ISignatureTransfer.TokenPermissions[] memory permissions =
new ISignatureTransfer.TokenPermissions[](order.inputs.length);
for (uint256 i = 0; i < order.inputs.length; i++) {
new ISignatureTransfer.TokenPermissions[](inputsRequired.length);

for (uint256 i = 0; i < inputsRequired.length; i++) {
permissions[i] = ISignatureTransfer.TokenPermissions({
token: address(order.inputs[i].token),
amount: order.inputs[i].amount
token: address(inputsRequired[i].token),
amount: uint256(inputsRequired[i].amount) // safe to cast here because we check above
});
}
return ISignatureTransfer.PermitBatchTransferFrom({
Expand All @@ -60,14 +37,31 @@ library Permit2Lib {
view
returns (ISignatureTransfer.SignatureTransferDetails[] memory)
{
InputTokenWithRecipient[] memory inputsRequired = getPositiveInputs(order.inputs);
ISignatureTransfer.SignatureTransferDetails[] memory details =
new ISignatureTransfer.SignatureTransferDetails[](order.inputs.length);
for (uint256 i = 0; i < order.inputs.length; i++) {
new ISignatureTransfer.SignatureTransferDetails[](inputsRequired.length);
for (uint256 i = 0; i < inputsRequired.length; i++) {
// if recipient is 0x0, use msg.sender
address recipient = order.inputs[i].recipient == address(0) ? msg.sender : order.inputs[i].recipient;
details[i] =
ISignatureTransfer.SignatureTransferDetails({to: recipient, requestedAmount: order.inputs[i].amount});
address recipient = inputsRequired[i].recipient == address(0) ? msg.sender : inputsRequired[i].recipient;
details[i] = ISignatureTransfer.SignatureTransferDetails({
to: recipient,
requestedAmount: uint256(inputsRequired[i].amount) // safe to cast here because we check above
});
}
return details;
}

function getPositiveInputs(InputTokenWithRecipient[] memory inputs)
internal
view
returns (InputTokenWithRecipient[] memory)
{
InputTokenWithRecipient[] memory positiveInputs = new InputTokenWithRecipient[](inputs.length);
for (uint256 i = 0; i < inputs.length; i++) {
if (inputs[i].amount > 0) {
positiveInputs[i] = inputs[i];
}
}
return positiveInputs;
}
}
32 changes: 31 additions & 1 deletion src/lib/RelayDecayLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,36 @@ library RelayDecayLib {
}
}

function decay(int256 startAmount, int256 endAmount, uint256 decayStartTime, uint256 decayEndTime)
internal
view
returns (int256 decayedAmount)
{
if (decayEndTime < decayStartTime) {
revert EndTimeBeforeStartTime();
} else if (decayEndTime <= block.timestamp) {
decayedAmount = endAmount;
} else if (decayStartTime >= block.timestamp) {
decayedAmount = startAmount;
} else {
unchecked {
uint256 elapsed = block.timestamp - decayStartTime;
uint256 duration = decayEndTime - decayStartTime;
if (endAmount < startAmount) {
decayedAmount =
startAmount - int256(uint256(abs(startAmount - endAmount)).mulDivDown(elapsed, duration));
} else {
decayedAmount =
startAmount + int256(uint256(abs(endAmount - startAmount)).mulDivDown(elapsed, duration));
}
}
}
}

function abs(int256 x) internal pure returns (int256) {
return x >= 0 ? x : -x;
}

/// @notice returns a decayed input array using the given decay spec and times
/// @param inputs The input array to decay
/// @param decayStartTime The time to start decaying
Expand Down Expand Up @@ -79,7 +109,7 @@ library RelayDecayLib {
revert IncorrectAmounts();
}

uint256 decayedInput = RelayDecayLib.decay(input.amount, input.maxAmount, decayStartTime, decayEndTime);
int256 decayedInput = RelayDecayLib.decay(input.amount, input.maxAmount, decayStartTime, decayEndTime);
result = InputTokenWithRecipient(input.token, decayedInput, input.maxAmount, input.recipient);
}
}
20 changes: 11 additions & 9 deletions src/lib/RelayOrderLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,32 @@ library RelayOrderLib {
using OrderInfoLib for OrderInfo;

bytes private constant INPUT_TOKEN_TYPE =
"InputTokenWithRecipient(address token,uint256 amount,uint256 maxAmount,address recipient)";
"InputTokenWithRecipient(address token,int256 amount,int256 maxAmount,address recipient)";

bytes32 private constant INPUT_TOKEN_TYPE_HASH = keccak256(INPUT_TOKEN_TYPE);

bytes internal constant ORDER_TYPE = abi.encodePacked(
bytes internal constant RELAY_ORDER_TYPE = abi.encodePacked(
"RelayOrder(",
"OrderInfo info,",
"uint256 decayStartTime,",
"uint256 decayEndTime,",
"bytes[] actions,",
"InputTokenWithRecipient[] inputs,",
"OutputToken[] outputs)",
OrderInfoLib.ORDER_INFO_TYPE,
INPUT_TOKEN_TYPE
"InputTokenWithRecipient[] inputs)"
);

bytes internal constant ORDER_TYPE =
abi.encodePacked(RELAY_ORDER_TYPE, INPUT_TOKEN_TYPE, OrderInfoLib.ORDER_INFO_TYPE);

bytes32 internal constant ORDER_TYPE_HASH = keccak256(ORDER_TYPE);

string private constant TOKEN_PERMISSIONS_TYPE = "TokenPermissions(address token,uint256 amount)";
string internal constant PERMIT2_ORDER_TYPE =
string(abi.encodePacked("RelayOrder witness)", ORDER_TYPE, TOKEN_PERMISSIONS_TYPE));
string internal constant PERMIT2_ORDER_TYPE = string(
abi.encodePacked("RelayOrder witness)", RELAY_ORDER_TYPE, OrderInfoLib.ORDER_INFO_TYPE, TOKEN_PERMISSIONS_TYPE)
);

/// @notice returns the hash of an input token struct
function hash(InputTokenWithRecipient memory input) private pure returns (bytes32) {
return keccak256(abi.encode(INPUT_TOKEN_TYPE_HASH, input.token, input.amount, input.maxAmount));
return keccak256(abi.encode(INPUT_TOKEN_TYPE_HASH, input.token, input.amount, input.maxAmount, input.recipient));
}

/// @notice returns the hash of an input token struct
Expand Down
3 changes: 3 additions & 0 deletions src/lib/ResolvedRelayOrderLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ library ResolvedRelayOrderLib {
/// @notice thrown if the order has expired
error DeadlinePassed();

/// @notice thrown if the order has incorrect inputs
error InvalidInputs();

/// @notice Validates a resolved order, reverting if invalid
/// @param filler The filler of the order
function validate(ResolvedRelayOrder memory resolvedOrder, address filler) internal view {
Expand Down
71 changes: 59 additions & 12 deletions src/reactors/RelayOrderReactor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import {ERC20} from "solmate/src/tokens/ERC20.sol";
import {IPermit2} from "permit2/src/interfaces/IPermit2.sol";
import {SignedOrder, OrderInfo} from "UniswapX/src/base/ReactorStructs.sol";
import {ReactorEvents} from "UniswapX/src/base/ReactorEvents.sol";
import {IRelayOrderReactorCallback} from "../interfaces/IRelayOrderReactorCallback.sol";
import {CurrencyLibrary} from "UniswapX/src/lib/CurrencyLibrary.sol";
import {IRelayOrderReactor} from "../interfaces/IRelayOrderReactor.sol";
import {IReactor} from "UniswapX/src/interfaces/IReactor.sol";
import {InputTokenWithRecipient, ResolvedRelayOrder} from "../base/ReactorStructs.sol";
import {ReactorErrors} from "../base/ReactorErrors.sol";
import {Permit2Lib} from "../lib/Permit2Lib.sol";
Expand All @@ -19,7 +20,7 @@ import {RelayDecayLib} from "../lib/RelayDecayLib.sol";
/// @notice Reactor for handling the execution of RelayOrders
/// @notice This contract MUST NOT have approvals or priviledged access
/// @notice any funds in this contract can be swept away by anyone
contract RelayOrderReactor is ReactorEvents, ReactorErrors, ReentrancyGuard, IRelayOrderReactor {
contract RelayOrderReactor is ReactorEvents, ReactorErrors, ReentrancyGuard, IReactor {
using SafeTransferLib for ERC20;
using CurrencyLibrary for address;
using Permit2Lib for ResolvedRelayOrder;
Expand All @@ -43,6 +44,22 @@ contract RelayOrderReactor is ReactorEvents, ReactorErrors, ReentrancyGuard, IRe
_fill(resolvedOrders);
}

/// @notice callbacks allow fillers to perform additional actions after the order is executed
/// example, to transfer in tokens to fill orders where users are owed additional amounts
function executeWithCallback(SignedOrder calldata order, bytes calldata callbackData)
external
payable
nonReentrant
{
ResolvedRelayOrder[] memory resolvedOrders = new ResolvedRelayOrder[](1);
resolvedOrders[0] = resolve(order);

_prepare(resolvedOrders);
_execute(resolvedOrders);
IRelayOrderReactorCallback(msg.sender).reactorCallback(resolvedOrders, callbackData);
_fill(resolvedOrders);
}

function executeBatch(SignedOrder[] calldata orders) external payable nonReentrant {
uint256 ordersLength = orders.length;
ResolvedRelayOrder[] memory resolvedOrders = new ResolvedRelayOrder[](ordersLength);
Expand All @@ -58,20 +75,44 @@ contract RelayOrderReactor is ReactorEvents, ReactorErrors, ReentrancyGuard, IRe
_fill(resolvedOrders);
}

/// @notice callbacks allow fillers to perform additional actions after the order is executed
/// example, to transfer in tokens to fill orders where users are owed additional amounts
function executeBatchWithCallback(SignedOrder[] calldata orders, bytes calldata callbackData)
external
payable
nonReentrant
{
uint256 ordersLength = orders.length;
ResolvedRelayOrder[] memory resolvedOrders = new ResolvedRelayOrder[](ordersLength);

unchecked {
for (uint256 i = 0; i < ordersLength; i++) {
resolvedOrders[i] = resolve(orders[i]);
}
}

_prepare(resolvedOrders);
_execute(resolvedOrders);
IRelayOrderReactorCallback(msg.sender).reactorCallback(resolvedOrders, callbackData);
_fill(resolvedOrders);
}

function _execute(ResolvedRelayOrder[] memory orders) internal {
uint256 ordersLength = orders.length;
// actions are encoded as (address target, uint256 value, bytes data)[]
for (uint256 i = 0; i < ordersLength;) {
ResolvedRelayOrder memory order = orders[i];
uint256 actionsLength = order.actions.length;
for (uint256 j = 0; j < actionsLength;) {
(address target, uint256 value, bytes memory data) =
abi.decode(order.actions[j], (address, uint256, bytes));
(bool success, bytes memory result) = target.call{value: value}(data);
if (!success) {
// bubble up all errors, including custom errors which are encoded like functions
assembly {
revert(add(result, 0x20), mload(result))
if (order.actions[j].length != 0) {
(address target, uint256 value, bytes memory data) =
abi.decode(order.actions[j], (address, uint256, bytes));
(bool success, bytes memory result) = target.call{value: value}(data);
if (!success) {
// bubble up all errors, including custom errors which are encoded like functions
assembly {
revert(add(result, 0x20), mload(result))
}
}
}
unchecked {
Expand Down Expand Up @@ -101,13 +142,21 @@ contract RelayOrderReactor is ReactorEvents, ReactorErrors, ReentrancyGuard, IRe
}

/// @notice emits a Fill event for each order
/// @notice all output token checks must be done in the encoded actions within the order
/// @notice any output token checks must be encoded in the order specified actions
/// @param orders The orders that have been filled
function _fill(ResolvedRelayOrder[] memory orders) internal {
uint256 ordersLength = orders.length;
unchecked {
for (uint256 i = 0; i < ordersLength; i++) {
ResolvedRelayOrder memory resolvedOrder = orders[i];
// If there are negative inputs, we must transfer those to the swapper from the reactor's balance
for (uint256 j = 0; j < resolvedOrder.inputs.length; j++) {
InputTokenWithRecipient memory input = resolvedOrder.inputs[j];
if (input.amount < 0) {
input.token.transfer(resolvedOrder.info.swapper, uint256(-input.amount));
}
}

emit Fill(orders[i].hash, msg.sender, resolvedOrder.info.swapper, resolvedOrder.info.nonce);
}
}
Expand Down Expand Up @@ -155,7 +204,5 @@ contract RelayOrderReactor is ReactorEvents, ReactorErrors, ReentrancyGuard, IRe
if (order.decayEndTime < order.decayStartTime) {
revert OrderEndTimeBeforeStartTime();
}

// TODO: add additional validations related to relayed actions, if desired
}
}
Loading
Loading