Skip to content

Commit

Permalink
Periphery update - calldata decoder (#396)
Browse files Browse the repository at this point in the history
* Periphery update - calldata decoder

* rename function and snapshots

* actually use the function
  • Loading branch information
hensha256 authored Sep 6, 2024
1 parent 2493262 commit a81e1ce
Show file tree
Hide file tree
Showing 9 changed files with 54 additions and 64 deletions.
5 changes: 3 additions & 2 deletions contracts/base/Dispatcher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import {ERC20} from 'solmate/src/tokens/ERC20.sol';
import {IAllowanceTransfer} from 'permit2/src/interfaces/IAllowanceTransfer.sol';
import {IERC721Permit} from '@uniswap/v3-periphery/contracts/interfaces/IERC721Permit.sol';
import {ActionConstants} from '@uniswap/v4-periphery/src/libraries/ActionConstants.sol';
import {CalldataDecoder} from '@uniswap/v4-periphery/src/libraries/CalldataDecoder.sol';

/// @title Decodes and Executes Commands
/// @notice Called by the UniversalRouter contract to efficiently decode and execute a singular command
abstract contract Dispatcher is Payments, V2SwapRouter, V3SwapRouter, V4SwapRouter, V3ToV4Migrator, Lock {
using BytesLib for bytes;
using CalldataDecoder for bytes;

error InvalidCommandType(uint256 commandType);
error BalanceTooLow();
Expand Down Expand Up @@ -279,8 +281,7 @@ abstract contract Dispatcher is Payments, V2SwapRouter, V3SwapRouter, V4SwapRout
} else {
// 0x21 <= command
if (command == Commands.EXECUTE_SUB_PLAN) {
bytes calldata _commands = inputs.toBytes(0);
bytes[] calldata _inputs = inputs.toBytesArray(1);
(bytes calldata _commands, bytes[] calldata _inputs) = inputs.decodeCommandsAndInputs();
(success, output) = (address(this)).call(abi.encodeCall(Dispatcher.execute, (_commands, _inputs)));
} else {
// placeholder area for commands 0x22-0x3f
Expand Down
27 changes: 7 additions & 20 deletions contracts/modules/uniswap/v3/BytesLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
pragma solidity ^0.8.0;

import {Constants} from '../../../libraries/Constants.sol';
import {CalldataDecoder} from '@uniswap/v4-periphery/src/libraries/CalldataDecoder.sol';

library BytesLib {
using CalldataDecoder for bytes;

error SliceOutOfBounds();

/// @notice Returns the address starting at byte 0
Expand Down Expand Up @@ -59,17 +62,6 @@ library BytesLib {
if (_bytes.length < length + relativeOffset) revert SliceOutOfBounds();
}

/// @notice Decode the `_arg`-th element in `_bytes` as `bytes`
/// @param _bytes The input bytes string to extract a bytes string from
/// @param _arg The index of the argument to extract
function toBytes(bytes calldata _bytes, uint256 _arg) internal pure returns (bytes calldata res) {
(uint256 length, uint256 offset) = toLengthOffset(_bytes, _arg);
assembly {
res.length := length
res.offset := offset
}
}

/// @notice Decode the `_arg`-th element in `_bytes` as `address[]`
/// @param _bytes The input bytes string to extract an address array from
/// @param _arg The index of the argument to extract
Expand All @@ -81,14 +73,9 @@ library BytesLib {
}
}

/// @notice Decode the `_arg`-th element in `_bytes` as `bytes[]`
/// @param _bytes The input bytes string to extract a bytes array from
/// @param _arg The index of the argument to extract
function toBytesArray(bytes calldata _bytes, uint256 _arg) internal pure returns (bytes[] calldata res) {
(uint256 length, uint256 offset) = toLengthOffset(_bytes, _arg);
assembly {
res.length := length
res.offset := offset
}
/// @notice Equivalent to abi.decode(bytes, bytes[])
/// @param _bytes The input bytes string to extract an parameters from
function decodeCommandsAndInputs(bytes calldata _bytes) internal pure returns (bytes calldata, bytes[] calldata) {
return _bytes.decodeActionsRouterParams();
}
}
2 changes: 2 additions & 0 deletions contracts/modules/uniswap/v3/V3SwapRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {SafeCast} from '@uniswap/v3-core/contracts/libraries/SafeCast.sol';
import {IUniswapV3Pool} from '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol';
import {IUniswapV3SwapCallback} from '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol';
import {ActionConstants} from '@uniswap/v4-periphery/src/libraries/ActionConstants.sol';
import {CalldataDecoder} from '@uniswap/v4-periphery/src/libraries/CalldataDecoder.sol';
import {Permit2Payments} from '../../Permit2Payments.sol';
import {UniswapImmutables} from '../UniswapImmutables.sol';
import {MaxInputAmount} from '../../../libraries/MaxInputAmount.sol';
Expand All @@ -16,6 +17,7 @@ import {ERC20} from 'solmate/src/tokens/ERC20.sol';
abstract contract V3SwapRouter is UniswapImmutables, Permit2Payments, IUniswapV3SwapCallback {
using V3Path for bytes;
using BytesLib for bytes;
using CalldataDecoder for bytes;
using SafeCast for uint256;

error V3InvalidSwap();
Expand Down
2 changes: 1 addition & 1 deletion lib/v4-periphery
Submodule v4-periphery updated 76 files
+1 −1 .forge-snapshots/BaseActionsRouter_mock10commands.snap
+1 −1 .forge-snapshots/Payments_swap_settleFromCaller_takeAllToMsgSender.snap
+1 −1 .forge-snapshots/Payments_swap_settleFromCaller_takeAllToSpecifiedAddress.snap
+1 −1 .forge-snapshots/Payments_swap_settleWithBalance_takeAllToMsgSender.snap
+1 −1 .forge-snapshots/Payments_swap_settleWithBalance_takeAllToSpecifiedAddress.snap
+1 −1 .forge-snapshots/PositionManager_burn_empty.snap
+1 −1 .forge-snapshots/PositionManager_burn_empty_native.snap
+1 −1 .forge-snapshots/PositionManager_burn_nonEmpty_native_withClose.snap
+1 −1 .forge-snapshots/PositionManager_burn_nonEmpty_native_withTakePair.snap
+1 −1 .forge-snapshots/PositionManager_burn_nonEmpty_withClose.snap
+1 −1 .forge-snapshots/PositionManager_burn_nonEmpty_withTakePair.snap
+1 −1 .forge-snapshots/PositionManager_collect_native.snap
+1 −1 .forge-snapshots/PositionManager_collect_sameRange.snap
+1 −1 .forge-snapshots/PositionManager_collect_withClose.snap
+1 −1 .forge-snapshots/PositionManager_collect_withTakePair.snap
+1 −1 .forge-snapshots/PositionManager_decreaseLiquidity_native.snap
+1 −1 .forge-snapshots/PositionManager_decreaseLiquidity_withClose.snap
+1 −1 .forge-snapshots/PositionManager_decreaseLiquidity_withTakePair.snap
+1 −1 .forge-snapshots/PositionManager_decrease_burnEmpty.snap
+1 −1 .forge-snapshots/PositionManager_decrease_burnEmpty_native.snap
+1 −1 .forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap
+1 −1 .forge-snapshots/PositionManager_decrease_take_take.snap
+1 −1 .forge-snapshots/PositionManager_increaseLiquidity_erc20_withClose.snap
+1 −1 .forge-snapshots/PositionManager_increaseLiquidity_erc20_withSettlePair.snap
+1 −1 .forge-snapshots/PositionManager_increaseLiquidity_native.snap
+1 −1 .forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap
+1 −1 .forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap
+1 −1 .forge-snapshots/PositionManager_increase_autocompound_clearExcess.snap
+1 −1 .forge-snapshots/PositionManager_mint_native.snap
+1 −1 .forge-snapshots/PositionManager_mint_nativeWithSweep_withClose.snap
+1 −1 .forge-snapshots/PositionManager_mint_nativeWithSweep_withSettlePair.snap
+1 −1 .forge-snapshots/PositionManager_mint_onSameTickLower.snap
+1 −1 .forge-snapshots/PositionManager_mint_onSameTickUpper.snap
+1 −1 .forge-snapshots/PositionManager_mint_sameRange.snap
+1 −1 .forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap
+1 −1 .forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap
+1 −1 .forge-snapshots/PositionManager_mint_withClose.snap
+1 −1 .forge-snapshots/PositionManager_mint_withSettlePair.snap
+1 −1 .forge-snapshots/PositionManager_multicall_initialize_mint.snap
+1 −1 .forge-snapshots/StateView_extsload_getFeeGrowthGlobals.snap
+1 −1 .forge-snapshots/StateView_extsload_getFeeGrowthInside.snap
+1 −1 .forge-snapshots/StateView_extsload_getPositionInfo.snap
+1 −1 .forge-snapshots/StateView_extsload_getTickFeeGrowthOutside.snap
+1 −1 .forge-snapshots/StateView_extsload_getTickInfo.snap
+1 −1 .forge-snapshots/V4Router_Bytecode.snap
+1 −1 .forge-snapshots/V4Router_ExactIn1Hop_nativeIn.snap
+1 −1 .forge-snapshots/V4Router_ExactIn1Hop_nativeOut.snap
+1 −1 .forge-snapshots/V4Router_ExactIn1Hop_oneForZero.snap
+1 −1 .forge-snapshots/V4Router_ExactIn1Hop_zeroForOne.snap
+1 −1 .forge-snapshots/V4Router_ExactIn2Hops.snap
+1 −1 .forge-snapshots/V4Router_ExactIn2Hops_nativeIn.snap
+1 −1 .forge-snapshots/V4Router_ExactIn3Hops.snap
+1 −1 .forge-snapshots/V4Router_ExactIn3Hops_nativeIn.snap
+1 −1 .forge-snapshots/V4Router_ExactInputSingle.snap
+1 −1 .forge-snapshots/V4Router_ExactInputSingle_nativeIn.snap
+1 −1 .forge-snapshots/V4Router_ExactInputSingle_nativeOut.snap
+1 −1 .forge-snapshots/V4Router_ExactOut1Hop_nativeIn_sweepETH.snap
+1 −1 .forge-snapshots/V4Router_ExactOut1Hop_nativeOut.snap
+1 −1 .forge-snapshots/V4Router_ExactOut1Hop_oneForZero.snap
+1 −1 .forge-snapshots/V4Router_ExactOut1Hop_zeroForOne.snap
+1 −1 .forge-snapshots/V4Router_ExactOut2Hops.snap
+1 −1 .forge-snapshots/V4Router_ExactOut2Hops_nativeIn.snap
+1 −1 .forge-snapshots/V4Router_ExactOut3Hops.snap
+1 −1 .forge-snapshots/V4Router_ExactOut3Hops_nativeIn.snap
+1 −1 .forge-snapshots/V4Router_ExactOut3Hops_nativeOut.snap
+1 −1 .forge-snapshots/V4Router_ExactOutputSingle.snap
+1 −1 .forge-snapshots/V4Router_ExactOutputSingle_nativeIn_sweepETH.snap
+1 −1 .forge-snapshots/V4Router_ExactOutputSingle_nativeOut.snap
+ audits/DRAFT_Spearbit_audit_UR.pdf
+ audits/DRAFT_Spearbit_audit_periphery.pdf
+ audits/OpenZeppelin_audit_periphery_universal_router.pdf
+5 −0 foundry.toml
+1 −1 lib/v4-core
+60 −43 src/libraries/CalldataDecoder.sol
+47 −0 test/libraries/CalldataDecoder.t.sol
+8 −0 test/mocks/MockCalldataDecoder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
exports[`Check Ownership Gas gas: balance check ERC20 1`] = `
Object {
"calldataByteLength": 356,
"gasUsed": 37701,
"gasUsed": 37698,
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -3,63 +3,63 @@
exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Batch reverts gas: 2 sub-plans, both fail but the transaction succeeds 1`] = `
Object {
"calldataByteLength": 1764,
"gasUsed": 269888,
"gasUsed": 270118,
}
`;

exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Batch reverts gas: 2 sub-plans, neither fails 1`] = `
Object {
"calldataByteLength": 1764,
"gasUsed": 245611,
"gasUsed": 245841,
}
`;

exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Batch reverts gas: 2 sub-plans, second sub plan fails 1`] = `
Object {
"calldataByteLength": 1764,
"gasUsed": 245611,
"gasUsed": 245841,
}
`;

exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Batch reverts gas: 2 sub-plans, the first fails 1`] = `
Object {
"calldataByteLength": 1764,
"gasUsed": 269888,
"gasUsed": 270118,
}
`;

exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Interleaving routes gas: V2, then V3 1`] = `
Object {
"calldataByteLength": 836,
"gasUsed": 189615,
"gasUsed": 189511,
}
`;

exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Interleaving routes gas: V3, then V2 1`] = `
Object {
"calldataByteLength": 836,
"gasUsed": 177198,
"gasUsed": 177094,
}
`;

exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V2 different routes, different input tokens, each two hop, with batch permit 1`] = `
Object {
"calldataByteLength": 1540,
"gasUsed": 297290,
"gasUsed": 297208,
}
`;

exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V2 different routes, each two hop, with explicit permit 1`] = `
Object {
"calldataByteLength": 1220,
"gasUsed": 308092,
"gasUsed": 308080,
}
`;

exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V2 different routes, each two hop, with explicit permit transfer from batch 1`] = `
Object {
"calldataByteLength": 1284,
"gasUsed": 309219,
"gasUsed": 309182,
}
`;

Expand All @@ -73,35 +73,35 @@ Object {
exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V3, one hop 1`] = `
Object {
"calldataByteLength": 996,
"gasUsed": 177042,
"gasUsed": 176938,
}
`;

exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ERC20 split V2 and V3, one hop, ADDRESS_THIS flag 1`] = `
Object {
"calldataByteLength": 996,
"gasUsed": 176817,
"gasUsed": 176713,
}
`;

exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ETH split V2 and V3, exactOut, one hop 1`] = `
Object {
"calldataByteLength": 964,
"gasUsed": 192252,
"gasUsed": 192148,
}
`;

exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ERC20 --> ETH split V2 and V3, one hop 1`] = `
Object {
"calldataByteLength": 964,
"gasUsed": 184916,
"gasUsed": 184812,
}
`;

exports[`Uniswap Gas Tests Mixing V2 and V3 with Universal Router. Split routes gas: ETH --> ERC20 split V2 and V3, one hop 1`] = `
Object {
"calldataByteLength": 1124,
"gasUsed": 191958,
"gasUsed": 191854,
}
`;

Expand Down Expand Up @@ -283,69 +283,69 @@ Object {
exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, one hop 1`] = `
Object {
"calldataByteLength": 516,
"gasUsed": 105581,
"gasUsed": 105477,
}
`;

exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, three hops 1`] = `
Object {
"calldataByteLength": 548,
"gasUsed": 254046,
"gasUsed": 253814,
}
`;

exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactIn, one trade, two hops 1`] = `
Object {
"calldataByteLength": 548,
"gasUsed": 177199,
"gasUsed": 177031,
}
`;

exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, one hop 1`] = `
Object {
"calldataByteLength": 516,
"gasUsed": 113016,
"gasUsed": 112912,
}
`;

exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, three hops 1`] = `
Object {
"calldataByteLength": 548,
"gasUsed": 249092,
"gasUsed": 248860,
}
`;

exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ERC20 gas: exactOut, one trade, two hops 1`] = `
Object {
"calldataByteLength": 548,
"gasUsed": 172758,
"gasUsed": 172590,
}
`;

exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ETH gas: exactIn swap 1`] = `
Object {
"calldataByteLength": 644,
"gasUsed": 121911,
"gasUsed": 121807,
}
`;

exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ERC20 --> ETH gas: exactOut swap 1`] = `
Object {
"calldataByteLength": 644,
"gasUsed": 129418,
"gasUsed": 129314,
}
`;

exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ETH --> ERC20 gas: exactIn swap 1`] = `
Object {
"calldataByteLength": 644,
"gasUsed": 215463,
"gasUsed": 215359,
}
`;

exports[`Uniswap Gas Tests Trade on UniswapV3 with Universal Router. ETH --> ERC20 gas: exactOut swap 1`] = `
Object {
"calldataByteLength": 772,
"gasUsed": 124599,
"gasUsed": 124495,
}
`;
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`UniversalRouter Gas Tests gas: bytecode size 1`] = `18973`;
exports[`UniversalRouter Gas Tests gas: bytecode size 1`] = `19173`;
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,32 @@ Object {
}
`;

exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps Permit2 Max Approval Swap 1`] = `1105016`;
exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps Permit2 Max Approval Swap 1`] = `1104136`;

exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps Permit2 Sign Per Swap 1`] = `1139458`;
exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps Permit2 Sign Per Swap 1`] = `1138498`;

exports[`Uniswap UX Tests gas: Comparisons Casual Swapper - 3 swaps SwapRouter02 1`] = `1124979`;

exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps Permit2 Max Approval Swap 1`] = `3083579`;
exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps Permit2 Max Approval Swap 1`] = `3081019`;

exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps Permit2 Sign Per Swap 1`] = `3237264`;
exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps Permit2 Sign Per Swap 1`] = `3234344`;

exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper - 10 swaps SwapRouter02 1`] = `3195011`;

exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions Permit2 Max Approval Swap 1`] = `4106041`;
exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions Permit2 Max Approval Swap 1`] = `4102561`;

exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions Permit2 Sign Per Swap 1`] = `4310247`;
exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions Permit2 Sign Per Swap 1`] = `4306287`;

exports[`Uniswap UX Tests gas: Comparisons Frequent Swapper across 3 swap router versions - 15 swaps across 3 versions SwapRouter02 1`] = `4282374`;

exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap Permit2 Max Approval Swap 1`] = `509124`;
exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap Permit2 Max Approval Swap 1`] = `508748`;

exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap Permit2 Sign Per Swap 1`] = `509442`;
exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap Permit2 Sign Per Swap 1`] = `509066`;

exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Complex Swap SwapRouter02 1`] = `500008`;

exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap Permit2 Max Approval Swap 1`] = `299776`;
exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap Permit2 Max Approval Swap 1`] = `299568`;

exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap Permit2 Sign Per Swap 1`] = `299712`;
exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap Permit2 Sign Per Swap 1`] = `299504`;

exports[`Uniswap UX Tests gas: Comparisons One Time Swapper - Simple Swap SwapRouter02 1`] = `270033`;
Original file line number Diff line number Diff line change
Expand Up @@ -31,34 +31,34 @@ Object {
exports[`V3 to V4 Migration Gas Tests V4 Commands increase gas: increase 1`] = `
Object {
"calldataByteLength": 2084,
"gasUsed": 227149,
"gasUsed": 228044,
}
`;

exports[`V3 to V4 Migration Gas Tests V4 Commands increase gas: migrate and increase 1`] = `
Object {
"calldataByteLength": 2980,
"gasUsed": 452177,
"gasUsed": 452893,
}
`;

exports[`V3 to V4 Migration Gas Tests V4 Commands mint gas: migrate and mint 1`] = `
Object {
"calldataByteLength": 2500,
"gasUsed": 590223,
"gasUsed": 591127,
}
`;

exports[`V3 to V4 Migration Gas Tests V4 Commands mint gas: migrate weth position into eth position with forwarding 1`] = `
Object {
"calldataByteLength": 2788,
"gasUsed": 594553,
"gasUsed": 595274,
}
`;

exports[`V3 to V4 Migration Gas Tests V4 Commands mint gas: mint 1`] = `
Object {
"calldataByteLength": 1604,
"gasUsed": 436651,
"gasUsed": 437555,
}
`;

0 comments on commit a81e1ce

Please sign in to comment.