feat: Proper USV2Executor transfer decoding + tests
- Properly decode, update tests with proper decoding - Added test case for each transfer method - Also fully tested permit2 transferFrom and it works perfectly. TODO: - Fix integration tests once encoding is implemented.
This commit is contained in:
committed by
Diana Carvalho
parent
8969186654
commit
ca1d474f08
@@ -14,9 +14,13 @@ contract ExecutorTransferMethods {
|
|||||||
IAllowanceTransfer public immutable permit2;
|
IAllowanceTransfer public immutable permit2;
|
||||||
|
|
||||||
enum TransferMethod {
|
enum TransferMethod {
|
||||||
|
// Assume funds are in the TychoRouter - transfer into the pool
|
||||||
TRANSFER,
|
TRANSFER,
|
||||||
|
// Assume funds are in msg.sender's wallet - transferFrom into the pool
|
||||||
TRANSFERFROM,
|
TRANSFERFROM,
|
||||||
|
// Assume funds are in msg.sender's wallet - permit2TransferFrom into the pool
|
||||||
TRANSFERPERMIT2,
|
TRANSFERPERMIT2,
|
||||||
|
// Assume funds have already been transferred into the pool. Do nothing.
|
||||||
NONE
|
NONE
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,10 +43,10 @@ contract ExecutorTransferMethods {
|
|||||||
} else if (method == TransferMethod.TRANSFERFROM) {
|
} else if (method == TransferMethod.TRANSFERFROM) {
|
||||||
tokenIn.safeTransferFrom(msg.sender, receiver, amount);
|
tokenIn.safeTransferFrom(msg.sender, receiver, amount);
|
||||||
} else if (method == TransferMethod.TRANSFERPERMIT2) {
|
} else if (method == TransferMethod.TRANSFERPERMIT2) {
|
||||||
// Permit2.permit is called from the TychoRouter
|
// Permit2.permit is already called from the TychoRouter
|
||||||
permit2.transferFrom(
|
permit2.transferFrom(
|
||||||
sender,
|
sender,
|
||||||
receiver, // Does this work if receiver is not address(this)?
|
receiver,
|
||||||
uint160(amount),
|
uint160(amount),
|
||||||
address(tokenIn)
|
address(tokenIn)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -70,15 +70,14 @@ contract UniswapV2Executor is IExecutor, ExecutorTransferMethods {
|
|||||||
TransferMethod method
|
TransferMethod method
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (data.length != 61) {
|
if (data.length != 62) {
|
||||||
revert UniswapV2Executor__InvalidDataLength();
|
revert UniswapV2Executor__InvalidDataLength();
|
||||||
}
|
}
|
||||||
inToken = IERC20(address(bytes20(data[0:20])));
|
inToken = IERC20(address(bytes20(data[0:20])));
|
||||||
target = address(bytes20(data[20:40]));
|
target = address(bytes20(data[20:40]));
|
||||||
receiver = address(bytes20(data[40:60]));
|
receiver = address(bytes20(data[40:60]));
|
||||||
zeroForOne = uint8(data[60]) > 0;
|
zeroForOne = uint8(data[60]) > 0;
|
||||||
// TODO properly decode, assume encoded using just 1 byte.
|
method = TransferMethod(uint8(data[61]));
|
||||||
method = TransferMethod.TRANSFER;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getAmountOut(address target, uint256 amountIn, bool zeroForOne)
|
function _getAmountOut(address target, uint256 amountIn, bool zeroForOne)
|
||||||
|
|||||||
@@ -97,11 +97,14 @@ contract TychoRouterTestSetup is Constants {
|
|||||||
address ekuboCore = 0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444;
|
address ekuboCore = 0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444;
|
||||||
|
|
||||||
IPoolManager poolManager = IPoolManager(poolManagerAddress);
|
IPoolManager poolManager = IPoolManager(poolManagerAddress);
|
||||||
usv2Executor = new UniswapV2Executor(factoryV2, initCodeV2, PERMIT2_ADDRESS);
|
usv2Executor =
|
||||||
usv3Executor = new UniswapV3Executor(factoryV3, initCodeV3, PERMIT2_ADDRESS);
|
new UniswapV2Executor(factoryV2, initCodeV2, PERMIT2_ADDRESS);
|
||||||
|
usv3Executor =
|
||||||
|
new UniswapV3Executor(factoryV3, initCodeV3, PERMIT2_ADDRESS);
|
||||||
usv4Executor = new UniswapV4Executor(poolManager);
|
usv4Executor = new UniswapV4Executor(poolManager);
|
||||||
pancakev3Executor =
|
pancakev3Executor = new UniswapV3Executor(
|
||||||
new UniswapV3Executor(factoryPancakeV3, initCodePancakeV3);
|
factoryPancakeV3, initCodePancakeV3, PERMIT2_ADDRESS
|
||||||
|
);
|
||||||
balancerv2Executor = new BalancerV2Executor();
|
balancerv2Executor = new BalancerV2Executor();
|
||||||
ekuboExecutor = new EkuboExecutor(ekuboCore);
|
ekuboExecutor = new EkuboExecutor(ekuboCore);
|
||||||
curveExecutor = new CurveExecutor(ETH_ADDR_FOR_CURVE);
|
curveExecutor = new CurveExecutor(ETH_ADDR_FOR_CURVE);
|
||||||
@@ -254,7 +257,13 @@ contract TychoRouterTestSetup is Constants {
|
|||||||
address receiver,
|
address receiver,
|
||||||
bool zero2one
|
bool zero2one
|
||||||
) internal pure returns (bytes memory) {
|
) internal pure returns (bytes memory) {
|
||||||
return abi.encodePacked(tokenIn, target, receiver, zero2one);
|
return abi.encodePacked(
|
||||||
|
tokenIn,
|
||||||
|
target,
|
||||||
|
receiver,
|
||||||
|
zero2one,
|
||||||
|
ExecutorTransferMethods.TransferMethod.TRANSFER
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function encodeUniswapV3Swap(
|
function encodeUniswapV3Swap(
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ contract UniswapV2ExecutorTest is Test, Constants {
|
|||||||
UniswapV2ExecutorExposed pancakeswapV2Exposed;
|
UniswapV2ExecutorExposed pancakeswapV2Exposed;
|
||||||
IERC20 WETH = IERC20(WETH_ADDR);
|
IERC20 WETH = IERC20(WETH_ADDR);
|
||||||
IERC20 DAI = IERC20(DAI_ADDR);
|
IERC20 DAI = IERC20(DAI_ADDR);
|
||||||
|
IAllowanceTransfer permit2;
|
||||||
|
|
||||||
function setUp() public {
|
function setUp() public {
|
||||||
uint256 forkBlock = 17323404;
|
uint256 forkBlock = 17323404;
|
||||||
@@ -64,16 +65,26 @@ contract UniswapV2ExecutorTest is Test, Constants {
|
|||||||
USV2_FACTORY_ETHEREUM, USV2_POOL_CODE_INIT_HASH, PERMIT2_ADDRESS
|
USV2_FACTORY_ETHEREUM, USV2_POOL_CODE_INIT_HASH, PERMIT2_ADDRESS
|
||||||
);
|
);
|
||||||
sushiswapV2Exposed = new UniswapV2ExecutorExposed(
|
sushiswapV2Exposed = new UniswapV2ExecutorExposed(
|
||||||
SUSHISWAPV2_FACTORY_ETHEREUM, SUSHIV2_POOL_CODE_INIT_HASH, PERMIT2_ADDRESS
|
SUSHISWAPV2_FACTORY_ETHEREUM,
|
||||||
|
SUSHIV2_POOL_CODE_INIT_HASH,
|
||||||
|
PERMIT2_ADDRESS
|
||||||
);
|
);
|
||||||
pancakeswapV2Exposed = new UniswapV2ExecutorExposed(
|
pancakeswapV2Exposed = new UniswapV2ExecutorExposed(
|
||||||
PANCAKESWAPV2_FACTORY_ETHEREUM, PANCAKEV2_POOL_CODE_INIT_HASH, PERMIT2_ADDRESS
|
PANCAKESWAPV2_FACTORY_ETHEREUM,
|
||||||
|
PANCAKEV2_POOL_CODE_INIT_HASH,
|
||||||
|
PERMIT2_ADDRESS
|
||||||
);
|
);
|
||||||
|
permit2 = IAllowanceTransfer(PERMIT2_ADDRESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testDecodeParams() public view {
|
function testDecodeParams() public view {
|
||||||
bytes memory params =
|
bytes memory params = abi.encodePacked(
|
||||||
abi.encodePacked(WETH_ADDR, address(2), address(3), false);
|
WETH_ADDR,
|
||||||
|
address(2),
|
||||||
|
address(3),
|
||||||
|
false,
|
||||||
|
ExecutorTransferMethods.TransferMethod.TRANSFER
|
||||||
|
);
|
||||||
|
|
||||||
(
|
(
|
||||||
IERC20 tokenIn,
|
IERC20 tokenIn,
|
||||||
@@ -137,12 +148,17 @@ contract UniswapV2ExecutorTest is Test, Constants {
|
|||||||
assertGe(amountOut, 0);
|
assertGe(amountOut, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testSwap() public {
|
function testSwapWithTransfer() public {
|
||||||
uint256 amountIn = 10 ** 18;
|
uint256 amountIn = 10 ** 18;
|
||||||
uint256 amountOut = 1847751195973566072891;
|
uint256 amountOut = 1847751195973566072891;
|
||||||
bool zeroForOne = false;
|
bool zeroForOne = false;
|
||||||
bytes memory protocolData =
|
bytes memory protocolData = abi.encodePacked(
|
||||||
abi.encodePacked(WETH_ADDR, WETH_DAI_POOL, BOB, zeroForOne);
|
WETH_ADDR,
|
||||||
|
WETH_DAI_POOL,
|
||||||
|
BOB,
|
||||||
|
zeroForOne,
|
||||||
|
uint8(ExecutorTransferMethods.TransferMethod.TRANSFER)
|
||||||
|
);
|
||||||
|
|
||||||
deal(WETH_ADDR, address(uniswapV2Exposed), amountIn);
|
deal(WETH_ADDR, address(uniswapV2Exposed), amountIn);
|
||||||
uniswapV2Exposed.swap(amountIn, protocolData);
|
uniswapV2Exposed.swap(amountIn, protocolData);
|
||||||
@@ -151,10 +167,162 @@ contract UniswapV2ExecutorTest is Test, Constants {
|
|||||||
assertGe(finalBalance, amountOut);
|
assertGe(finalBalance, amountOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function testSwapWithTransferFrom() public {
|
||||||
|
uint256 amountIn = 10 ** 18;
|
||||||
|
uint256 amountOut = 1847751195973566072891;
|
||||||
|
bool zeroForOne = false;
|
||||||
|
bytes memory protocolData = abi.encodePacked(
|
||||||
|
WETH_ADDR,
|
||||||
|
WETH_DAI_POOL,
|
||||||
|
BOB,
|
||||||
|
zeroForOne,
|
||||||
|
uint8(ExecutorTransferMethods.TransferMethod.TRANSFERFROM)
|
||||||
|
);
|
||||||
|
|
||||||
|
deal(WETH_ADDR, address(this), amountIn);
|
||||||
|
IERC20(WETH_ADDR).approve(address(uniswapV2Exposed), amountIn);
|
||||||
|
|
||||||
|
uniswapV2Exposed.swap(amountIn, protocolData);
|
||||||
|
|
||||||
|
uint256 finalBalance = DAI.balanceOf(BOB);
|
||||||
|
assertGe(finalBalance, amountOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO generalize these next two methods - don't reuse from TychoRouterTestSetup
|
||||||
|
/**
|
||||||
|
* @dev Handles the Permit2 approval process for Alice, allowing the TychoRouter contract
|
||||||
|
* to spend `amount_in` of `tokenIn` on her behalf.
|
||||||
|
*
|
||||||
|
* This function approves the Permit2 contract to transfer the specified token amount
|
||||||
|
* and constructs a `PermitSingle` struct for the approval. It also generates a valid
|
||||||
|
* EIP-712 signature for the approval using Alice's private key.
|
||||||
|
*
|
||||||
|
* @param tokenIn The address of the token being approved.
|
||||||
|
* @param amount_in The amount of tokens to approve for transfer.
|
||||||
|
* @return permitSingle The `PermitSingle` struct containing the approval details.
|
||||||
|
* @return signature The EIP-712 signature for the approval.
|
||||||
|
*/
|
||||||
|
function handlePermit2Approval(address tokenIn, uint256 amount_in)
|
||||||
|
internal
|
||||||
|
returns (IAllowanceTransfer.PermitSingle memory, bytes memory)
|
||||||
|
{
|
||||||
|
IERC20(tokenIn).approve(PERMIT2_ADDRESS, amount_in);
|
||||||
|
IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer
|
||||||
|
.PermitSingle({
|
||||||
|
details: IAllowanceTransfer.PermitDetails({
|
||||||
|
token: tokenIn,
|
||||||
|
amount: uint160(amount_in),
|
||||||
|
expiration: uint48(block.timestamp + 1 days),
|
||||||
|
nonce: 0
|
||||||
|
}),
|
||||||
|
spender: address(uniswapV2Exposed),
|
||||||
|
sigDeadline: block.timestamp + 1 days
|
||||||
|
});
|
||||||
|
|
||||||
|
bytes memory signature = signPermit2(permitSingle, ALICE_PK);
|
||||||
|
return (permitSingle, signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Signs a Permit2 `PermitSingle` struct with the given private key.
|
||||||
|
* @param permit The `PermitSingle` struct to sign.
|
||||||
|
* @param privateKey The private key of the signer.
|
||||||
|
* @return The signature as a `bytes` array.
|
||||||
|
*/
|
||||||
|
function signPermit2(
|
||||||
|
IAllowanceTransfer.PermitSingle memory permit,
|
||||||
|
uint256 privateKey
|
||||||
|
) internal view returns (bytes memory) {
|
||||||
|
bytes32 _PERMIT_DETAILS_TYPEHASH = keccak256(
|
||||||
|
"PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"
|
||||||
|
);
|
||||||
|
bytes32 _PERMIT_SINGLE_TYPEHASH = keccak256(
|
||||||
|
"PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"
|
||||||
|
);
|
||||||
|
bytes32 domainSeparator = keccak256(
|
||||||
|
abi.encode(
|
||||||
|
keccak256(
|
||||||
|
"EIP712Domain(string name,uint256 chainId,address verifyingContract)"
|
||||||
|
),
|
||||||
|
keccak256("Permit2"),
|
||||||
|
block.chainid,
|
||||||
|
PERMIT2_ADDRESS
|
||||||
|
)
|
||||||
|
);
|
||||||
|
bytes32 detailsHash =
|
||||||
|
keccak256(abi.encode(_PERMIT_DETAILS_TYPEHASH, permit.details));
|
||||||
|
bytes32 permitHash = keccak256(
|
||||||
|
abi.encode(
|
||||||
|
_PERMIT_SINGLE_TYPEHASH,
|
||||||
|
detailsHash,
|
||||||
|
permit.spender,
|
||||||
|
permit.sigDeadline
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
bytes32 digest =
|
||||||
|
keccak256(abi.encodePacked("\x19\x01", domainSeparator, permitHash));
|
||||||
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest);
|
||||||
|
|
||||||
|
return abi.encodePacked(r, s, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function testSwapWithPermit2TransferFrom() public {
|
||||||
|
uint256 amountIn = 10 ** 18;
|
||||||
|
uint256 amountOut = 1847751195973566072891;
|
||||||
|
bool zeroForOne = false;
|
||||||
|
bytes memory protocolData = abi.encodePacked(
|
||||||
|
WETH_ADDR,
|
||||||
|
WETH_DAI_POOL,
|
||||||
|
ALICE,
|
||||||
|
zeroForOne,
|
||||||
|
uint8(ExecutorTransferMethods.TransferMethod.TRANSFERPERMIT2)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
deal(WETH_ADDR, ALICE, amountIn);
|
||||||
|
vm.startPrank(ALICE);
|
||||||
|
(
|
||||||
|
IAllowanceTransfer.PermitSingle memory permitSingle,
|
||||||
|
bytes memory signature
|
||||||
|
) = handlePermit2Approval(WETH_ADDR, amountIn);
|
||||||
|
|
||||||
|
// Assume the permit2.approve method will be called from the TychoRouter
|
||||||
|
// Replicate this secnario in this test.
|
||||||
|
permit2.permit(ALICE, permitSingle, signature);
|
||||||
|
|
||||||
|
uniswapV2Exposed.swap(amountIn, protocolData);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
uint256 finalBalance = DAI.balanceOf(ALICE);
|
||||||
|
assertGe(finalBalance, amountOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSwapNoTransfer() public {
|
||||||
|
uint256 amountIn = 10 ** 18;
|
||||||
|
uint256 amountOut = 1847751195973566072891;
|
||||||
|
bool zeroForOne = false;
|
||||||
|
bytes memory protocolData = abi.encodePacked(
|
||||||
|
WETH_ADDR,
|
||||||
|
WETH_DAI_POOL,
|
||||||
|
BOB,
|
||||||
|
zeroForOne,
|
||||||
|
uint8(ExecutorTransferMethods.TransferMethod.NONE)
|
||||||
|
);
|
||||||
|
|
||||||
|
deal(WETH_ADDR, address(this), amountIn);
|
||||||
|
IERC20(WETH_ADDR).transfer(address(WETH_DAI_POOL), amountIn);
|
||||||
|
uniswapV2Exposed.swap(amountIn, protocolData);
|
||||||
|
|
||||||
|
uint256 finalBalance = DAI.balanceOf(BOB);
|
||||||
|
assertGe(finalBalance, amountOut);
|
||||||
|
}
|
||||||
|
|
||||||
function testDecodeIntegration() public view {
|
function testDecodeIntegration() public view {
|
||||||
// Generated by the ExecutorStrategyEncoder - test_executor_strategy_encode
|
// Generated by the ExecutorStrategyEncoder - test_executor_strategy_encode
|
||||||
bytes memory protocolData =
|
bytes memory protocolData =
|
||||||
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc288e6a0c2ddd26feeb64f039a2c41296fcb3f5640000000000000000000000000000000000000000100";
|
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc288e6a0c2ddd26feeb64f039a2c41296fcb3f564000000000000000000000000000000000000000010000";
|
||||||
|
|
||||||
(
|
(
|
||||||
IERC20 tokenIn,
|
IERC20 tokenIn,
|
||||||
@@ -175,7 +343,7 @@ contract UniswapV2ExecutorTest is Test, Constants {
|
|||||||
function testSwapIntegration() public {
|
function testSwapIntegration() public {
|
||||||
// Generated by the ExecutorStrategyEncoder - test_executor_strategy_encode
|
// Generated by the ExecutorStrategyEncoder - test_executor_strategy_encode
|
||||||
bytes memory protocolData =
|
bytes memory protocolData =
|
||||||
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb111d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e00";
|
hex"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb111d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0000";
|
||||||
uint256 amountIn = 10 ** 18;
|
uint256 amountIn = 10 ** 18;
|
||||||
uint256 amountOut = 1847751195973566072891;
|
uint256 amountOut = 1847751195973566072891;
|
||||||
deal(WETH_ADDR, address(uniswapV2Exposed), amountIn);
|
deal(WETH_ADDR, address(uniswapV2Exposed), amountIn);
|
||||||
@@ -189,8 +357,13 @@ contract UniswapV2ExecutorTest is Test, Constants {
|
|||||||
uint256 amountIn = 10 ** 18;
|
uint256 amountIn = 10 ** 18;
|
||||||
bool zeroForOne = false;
|
bool zeroForOne = false;
|
||||||
address fakePool = address(new FakeUniswapV2Pool(WETH_ADDR, DAI_ADDR));
|
address fakePool = address(new FakeUniswapV2Pool(WETH_ADDR, DAI_ADDR));
|
||||||
bytes memory protocolData =
|
bytes memory protocolData = abi.encodePacked(
|
||||||
abi.encodePacked(WETH_ADDR, fakePool, BOB, zeroForOne);
|
WETH_ADDR,
|
||||||
|
fakePool,
|
||||||
|
BOB,
|
||||||
|
zeroForOne,
|
||||||
|
uint8(ExecutorTransferMethods.TransferMethod.TRANSFER)
|
||||||
|
);
|
||||||
|
|
||||||
deal(WETH_ADDR, address(uniswapV2Exposed), amountIn);
|
deal(WETH_ADDR, address(uniswapV2Exposed), amountIn);
|
||||||
vm.expectRevert(UniswapV2Executor__InvalidTarget.selector);
|
vm.expectRevert(UniswapV2Executor__InvalidTarget.selector);
|
||||||
@@ -204,8 +377,13 @@ contract UniswapV2ExecutorTest is Test, Constants {
|
|||||||
vm.rollFork(26857267);
|
vm.rollFork(26857267);
|
||||||
uint256 amountIn = 10 * 10 ** 6;
|
uint256 amountIn = 10 * 10 ** 6;
|
||||||
bool zeroForOne = true;
|
bool zeroForOne = true;
|
||||||
bytes memory protocolData =
|
bytes memory protocolData = abi.encodePacked(
|
||||||
abi.encodePacked(BASE_USDC, USDC_MAG7_POOL, BOB, zeroForOne);
|
BASE_USDC,
|
||||||
|
USDC_MAG7_POOL,
|
||||||
|
BOB,
|
||||||
|
zeroForOne,
|
||||||
|
uint8(ExecutorTransferMethods.TransferMethod.TRANSFER)
|
||||||
|
);
|
||||||
|
|
||||||
deal(BASE_USDC, address(uniswapV2Exposed), amountIn);
|
deal(BASE_USDC, address(uniswapV2Exposed), amountIn);
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ contract UniswapV3ExecutorTest is Test, Constants {
|
|||||||
USV3_FACTORY_ETHEREUM, USV3_POOL_CODE_INIT_HASH, PERMIT2_ADDRESS
|
USV3_FACTORY_ETHEREUM, USV3_POOL_CODE_INIT_HASH, PERMIT2_ADDRESS
|
||||||
);
|
);
|
||||||
pancakeV3Exposed = new UniswapV3ExecutorExposed(
|
pancakeV3Exposed = new UniswapV3ExecutorExposed(
|
||||||
PANCAKESWAPV3_DEPLOYER_ETHEREUM, PANCAKEV3_POOL_CODE_INIT_HASH, PERMIT2_ADDRESS
|
PANCAKESWAPV3_DEPLOYER_ETHEREUM,
|
||||||
|
PANCAKEV3_POOL_CODE_INIT_HASH,
|
||||||
|
PERMIT2_ADDRESS
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user