Skip to content

Commit

Permalink
feat: prevent permit2.permit frontrun (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
ChefMist authored Sep 13, 2024
1 parent cc2d2e2 commit dcbb68a
Show file tree
Hide file tree
Showing 9 changed files with 222 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -1 +1 @@
152658
152659
2 changes: 1 addition & 1 deletion .forge-snapshots/UniversalRouterBytecodeSize.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
24108
24160
2 changes: 1 addition & 1 deletion .forge-snapshots/UniversalRouterTest#test_sweep_token.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
55428
55441
Original file line number Diff line number Diff line change
@@ -1 +1 @@
559336
559373
Original file line number Diff line number Diff line change
@@ -1 +1 @@
594165
594191
Original file line number Diff line number Diff line change
@@ -1 +1 @@
569991
570028
Original file line number Diff line number Diff line change
@@ -1 +1 @@
583518
583544
12 changes: 10 additions & 2 deletions src/base/Dispatcher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,11 @@ abstract contract Dispatcher is
permitBatch := add(inputs.offset, calldataload(inputs.offset))
}
bytes calldata data = inputs.toBytes(1);
PERMIT2.permit(msgSender(), permitBatch, data);
try PERMIT2.permit(msgSender(), permitBatch, data) {}
catch (bytes memory reason) {
output = reason;
success = false;
}
return (success, output);
} else if (command == Commands.SWEEP) {
// equivalent: abi.decode(inputs, (address, address, uint256))
Expand Down Expand Up @@ -204,7 +208,11 @@ abstract contract Dispatcher is
permitSingle := inputs.offset
}
bytes calldata data = inputs.toBytes(6); // PermitSingle takes first 6 slots (0..5)
PERMIT2.permit(msgSender(), permitSingle, data);
try PERMIT2.permit(msgSender(), permitSingle, data) {}
catch (bytes memory reason) {
output = reason;
success = false;
}
return (success, output);
} else if (command == Commands.WRAP_ETH) {
// equivalent: abi.decode(inputs, (address, uint256))
Expand Down
207 changes: 205 additions & 2 deletions test/UniversalRouter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol";
import {ERC20} from "solmate/src/tokens/ERC20.sol";
import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol";
import {ActionConstants} from "pancake-v4-periphery/src/libraries/ActionConstants.sol";
import {Permit2SignatureHelpers} from "pancake-v4-periphery/test/shared/Permit2SignatureHelpers.sol";
import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";
import {DeployPermit2} from "permit2/test/utils/DeployPermit2.sol";
import {WETH} from "solmate/src/tokens/WETH.sol";

import {UniversalRouter} from "../src/UniversalRouter.sol";
Expand All @@ -19,21 +21,26 @@ import {MockERC721} from "./mock/MockERC721.sol";
import {MockERC1155} from "./mock/MockERC1155.sol";
import {RouterParameters} from "../src/base/RouterImmutables.sol";

contract UniversalRouterTest is Test, GasSnapshot {
contract UniversalRouterTest is Test, GasSnapshot, Permit2SignatureHelpers, DeployPermit2 {
error ContractSizeTooLarge(uint256 diff);
error InvalidNonce();

address RECIPIENT = makeAddr("alice");
uint256 constant AMOUNT = 10 ** 18;

UniversalRouter router;
MockERC20 erc20;
MockERC20 erc20_2;
MockERC721 erc721;
MockERC1155 erc1155;
WETH weth9 = new WETH();
IAllowanceTransfer permit2;

function setUp() public {
permit2 = IAllowanceTransfer(deployPermit2());

RouterParameters memory params = RouterParameters({
permit2: address(0),
permit2: address(permit2),
weth9: address(weth9),
v2Factory: address(0),
v3Factory: address(0),
Expand All @@ -53,6 +60,7 @@ contract UniversalRouterTest is Test, GasSnapshot {

router = new UniversalRouter(params);
erc20 = new MockERC20();
erc20_2 = new MockERC20();
erc721 = new MockERC721();
erc1155 = new MockERC1155();
}
Expand Down Expand Up @@ -197,4 +205,199 @@ contract UniversalRouterTest is Test, GasSnapshot {
vm.expectRevert(Payments.InsufficientETH.selector);
router.execute{value: 1 ether}(commands, inputs);
}

function test_permit2Single() public {
// pre-req:
(address charlie, uint256 charliePK) = makeAddrAndKey("charlie");
uint160 permitAmount = type(uint160).max;
uint48 permitExpiration = uint48(block.timestamp + 10e18);
uint48 permitNonce = 0;

IAllowanceTransfer.PermitSingle memory permit =
defaultERC20PermitAllowance(address(erc20), permitAmount, permitExpiration, permitNonce);
permit.spender = address(router);
bytes memory sig = getPermitSignature(permit, charliePK, permit2.DOMAIN_SEPARATOR());

// before verify
(uint160 _amount, uint48 _expiration, uint48 _nonce) =
permit2.allowance(charlie, address(erc20), address(router));
assertEq(_amount, 0);
assertEq(_expiration, 0);
assertEq(_nonce, 0);

// execute
vm.startPrank(charlie);
bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.PERMIT2_PERMIT)));
bytes[] memory inputs = new bytes[](1);
inputs[0] = abi.encode(permit, sig);
router.execute(commands, inputs);

// after verify
(_amount, _expiration, _nonce) = permit2.allowance(charlie, address(erc20), address(router));
assertEq(_amount, permitAmount);
assertEq(_expiration, permitExpiration);
assertEq(_nonce, permitNonce + 1);
}

function test_permit2Batch() public {
// pre-req:
(address charlie, uint256 charliePK) = makeAddrAndKey("charlie");
uint160 permitAmount = type(uint160).max;
uint48 permitExpiration = uint48(block.timestamp + 10e18);
uint48 permitNonce = 0;

address[] memory tokens = new address[](2);
tokens[0] = address(erc20);
tokens[1] = address(erc20_2);

IAllowanceTransfer.PermitBatch memory permit =
defaultERC20PermitBatchAllowance(tokens, permitAmount, permitExpiration, permitNonce);
permit.spender = address(router);
bytes memory sig = getPermitBatchSignature(permit, charliePK, permit2.DOMAIN_SEPARATOR());

// before verify
for (uint256 i; i < tokens.length; i++) {
(uint160 _amount, uint48 _expiration, uint48 _nonce) =
permit2.allowance(charlie, tokens[i], address(router));
assertEq(_amount, 0);
assertEq(_expiration, 0);
assertEq(_nonce, 0);
}

// execute
vm.startPrank(charlie);
bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.PERMIT2_PERMIT_BATCH)));
bytes[] memory inputs = new bytes[](1);
inputs[0] = abi.encode(permit, sig);
router.execute(commands, inputs);

// after verify
for (uint256 i; i < tokens.length; i++) {
(uint160 _amount, uint48 _expiration, uint48 _nonce) =
permit2.allowance(charlie, address(tokens[i]), address(router));
assertEq(_amount, permitAmount);
assertEq(_expiration, permitExpiration);
assertEq(_nonce, permitNonce + 1);
}
}

/// @dev test showing that if permit command have ALLOW_REVERT flag and was front-run, the next command can still execute
function test_permit2Single_frontrun() public {
// pre-req
address bob = makeAddr("bob");
(address charlie, uint256 charliePK) = makeAddrAndKey("charlie");
uint160 permitAmount = type(uint160).max;
uint48 permitExpiration = uint48(block.timestamp + 10e18);
uint48 permitNonce = 0;

IAllowanceTransfer.PermitSingle memory permit =
defaultERC20PermitAllowance(address(erc20), permitAmount, permitExpiration, permitNonce);
permit.spender = address(router);
bytes memory sig = getPermitSignature(permit, charliePK, permit2.DOMAIN_SEPARATOR());

// bob front-runs the permits
vm.prank(bob);
permit2.permit(charlie, permit, sig);

// bob's front-run was successful
(uint160 _amount, uint48 _expiration, uint48 _nonce) =
permit2.allowance(charlie, address(erc20), address(router));
assertEq(_amount, permitAmount);
assertEq(_expiration, permitExpiration);
assertEq(_nonce, permitNonce + 1);

// before
assertEq(weth9.balanceOf(address(router)), 0);

// charlie tries to call universal router permit2_permit and wrap_eth command
vm.deal(charlie, 1 ether);
vm.startPrank(charlie);

bytes[] memory inputs = new bytes[](2);
inputs[0] = abi.encode(permit, sig);
inputs[1] = abi.encode(ActionConstants.ADDRESS_THIS, ActionConstants.CONTRACT_BALANCE);

bytes memory commands;

// attempt 1: execute and expect revert
commands = abi.encodePacked(bytes1(uint8(Commands.PERMIT2_PERMIT)), bytes1(uint8(Commands.WRAP_ETH)));
vm.expectRevert(
abi.encodeWithSelector(
IUniversalRouter.ExecutionFailed.selector, 0, abi.encodePacked(InvalidNonce.selector)
)
);
router.execute{value: 1 ether}(commands, inputs);

// attempt 2: execute with allow revert flag and no revert expected
commands = abi.encodePacked(
bytes1(uint8(Commands.PERMIT2_PERMIT)) | Commands.FLAG_ALLOW_REVERT, bytes1(uint8(Commands.WRAP_ETH))
);
router.execute{value: 1 ether}(commands, inputs);

// after
assertEq(weth9.balanceOf(address(router)), 1 ether);
}

/// @dev test showing that if permit command have ALLOW_REVERT flag and was front-run, the next command can still execute
function test_permit2Batch_frontrun() public {
// pre-req
address bob = makeAddr("bob");
(address charlie, uint256 charliePK) = makeAddrAndKey("charlie");
uint160 permitAmount = type(uint160).max;
uint48 permitExpiration = uint48(block.timestamp + 10e18);
uint48 permitNonce = 0;

address[] memory tokens = new address[](2);
tokens[0] = address(erc20);
tokens[1] = address(erc20_2);

IAllowanceTransfer.PermitBatch memory permit =
defaultERC20PermitBatchAllowance(tokens, permitAmount, permitExpiration, permitNonce);
permit.spender = address(router);
bytes memory sig = getPermitBatchSignature(permit, charliePK, permit2.DOMAIN_SEPARATOR());

// bob front-runs the permits
vm.prank(bob);
permit2.permit(charlie, permit, sig);

// bob's front-run was successful
for (uint256 i; i < tokens.length; i++) {
(uint160 _amount, uint48 _expiration, uint48 _nonce) =
permit2.allowance(charlie, address(tokens[i]), address(router));
assertEq(_amount, permitAmount);
assertEq(_expiration, permitExpiration);
assertEq(_nonce, permitNonce + 1);
}

// before
assertEq(weth9.balanceOf(address(router)), 0);

// charlie tries to call universal router permit2_permit and wrap_eth command
vm.deal(charlie, 1 ether);
vm.startPrank(charlie);

bytes[] memory inputs = new bytes[](2);
inputs[0] = abi.encode(permit, sig);
inputs[1] = abi.encode(ActionConstants.ADDRESS_THIS, ActionConstants.CONTRACT_BALANCE);

bytes memory commands;

// attempt 1: execute and expect revert
commands = abi.encodePacked(bytes1(uint8(Commands.PERMIT2_PERMIT_BATCH)), bytes1(uint8(Commands.WRAP_ETH)));
vm.expectRevert(
abi.encodeWithSelector(
IUniversalRouter.ExecutionFailed.selector, 0, abi.encodePacked(InvalidNonce.selector)
)
);
router.execute{value: 1 ether}(commands, inputs);

// attempt 2: execute with allow revert flag and no revert expected
commands = abi.encodePacked(
bytes1(uint8(Commands.PERMIT2_PERMIT_BATCH)) | Commands.FLAG_ALLOW_REVERT, bytes1(uint8(Commands.WRAP_ETH))
);
router.execute{value: 1 ether}(commands, inputs);

// after
assertEq(weth9.balanceOf(address(router)), 1 ether);
}
}

0 comments on commit dcbb68a

Please sign in to comment.