434 lines
20 KiB
Solidity
434 lines
20 KiB
Solidity
|
|
// SPDX-License-Identifier: MIT
|
|||
|
|
pragma solidity ^0.8.19;
|
|||
|
|
|
|||
|
|
import { Kraiken } from "../../src/Kraiken.sol";
|
|||
|
|
import { LiquidityManager } from "../../src/LiquidityManager.sol";
|
|||
|
|
import { Stake } from "../../src/Stake.sol";
|
|||
|
|
import { ThreePositionStrategy } from "../../src/abstracts/ThreePositionStrategy.sol";
|
|||
|
|
import { UniswapHelpers } from "../../src/helpers/UniswapHelpers.sol";
|
|||
|
|
import { IWETH9 } from "../../src/interfaces/IWETH9.sol";
|
|||
|
|
import { TestEnvironment } from "../../test/helpers/TestBase.sol";
|
|||
|
|
|
|||
|
|
import { BackgroundLP } from "./BackgroundLP.sol";
|
|||
|
|
import { SwapExecutor } from "./SwapExecutor.sol";
|
|||
|
|
import { IUniswapV3Factory } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
|
|||
|
|
import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
|||
|
|
import "forge-std/Script.sol";
|
|||
|
|
import "forge-std/console2.sol";
|
|||
|
|
|
|||
|
|
/// @title FuzzingBase
|
|||
|
|
/// @notice Shared infrastructure for all fuzzing/analysis scripts.
|
|||
|
|
/// @dev Provides environment setup, trade execution, liquidation, token recovery,
|
|||
|
|
/// and CSV parsing. Concrete scripts inherit this and implement their own run() logic.
|
|||
|
|
abstract contract FuzzingBase is Script {
|
|||
|
|
// ── Constants ──────────────────────────────────────────────────────────
|
|||
|
|
uint256 internal constant LM_FUNDING_ETH = 200 ether;
|
|||
|
|
uint256 internal constant LM_INITIAL_WETH = 100 ether;
|
|||
|
|
uint256 internal constant RECENTER_GAS_LIMIT = 50_000_000;
|
|||
|
|
uint256 internal constant RECENTER_TIME_ADVANCE = 1 hours;
|
|||
|
|
uint256 internal constant LIQUIDATION_MAX_ATTEMPTS = 20;
|
|||
|
|
uint256 internal constant LIQUIDATION_RECENTER_INTERVAL = 3;
|
|||
|
|
address internal constant DEAD_ADDRESS = address(0xdead);
|
|||
|
|
uint256 internal constant SELL_PCT_MIN = 20;
|
|||
|
|
uint256 internal constant SELL_PCT_RANGE = 60;
|
|||
|
|
|
|||
|
|
// ── Environment ───────────────────────────────────────────────────────
|
|||
|
|
TestEnvironment internal testEnv;
|
|||
|
|
IUniswapV3Factory internal factory;
|
|||
|
|
IUniswapV3Pool internal pool;
|
|||
|
|
IWETH9 internal weth;
|
|||
|
|
Kraiken internal kraiken;
|
|||
|
|
Stake internal stake;
|
|||
|
|
LiquidityManager internal lm;
|
|||
|
|
SwapExecutor internal swapExecutor;
|
|||
|
|
bool internal token0isWeth;
|
|||
|
|
|
|||
|
|
// ── Actors ────────────────────────────────────────────────────────────
|
|||
|
|
address internal trader = makeAddr("trader");
|
|||
|
|
address internal fees = makeAddr("fees");
|
|||
|
|
BackgroundLP internal backgroundLP;
|
|||
|
|
|
|||
|
|
// ══════════════════════════════════════════════════════════════════════
|
|||
|
|
// Environment Setup
|
|||
|
|
// ══════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
/// @notice Deploy TestEnvironment and Uniswap factory (call once per script)
|
|||
|
|
function _initInfrastructure() internal {
|
|||
|
|
testEnv = new TestEnvironment(fees);
|
|||
|
|
factory = UniswapHelpers.deployUniswapFactory();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// @notice Deploy a fresh pool/LM/optimizer environment
|
|||
|
|
/// @param optimizer Address of the optimizer contract to use
|
|||
|
|
/// @param wethIsToken0 Token ordering for the pool
|
|||
|
|
/// @param uncapped Whether to deploy SwapExecutor in uncapped mode
|
|||
|
|
function _setupEnvironment(address optimizer, bool wethIsToken0, bool uncapped) internal {
|
|||
|
|
(factory, pool, weth, kraiken, stake, lm,, token0isWeth) = testEnv.setupEnvironmentWithExistingFactory(factory, wethIsToken0, fees, optimizer);
|
|||
|
|
|
|||
|
|
swapExecutor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm, uncapped);
|
|||
|
|
|
|||
|
|
vm.deal(address(lm), LM_FUNDING_ETH);
|
|||
|
|
vm.prank(address(lm));
|
|||
|
|
weth.deposit{ value: LM_INITIAL_WETH }();
|
|||
|
|
|
|||
|
|
vm.prank(fees);
|
|||
|
|
try lm.recenter() { } catch { }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// @notice Overload: setup with custom LM funding
|
|||
|
|
function _setupEnvironment(address optimizer, bool wethIsToken0, bool uncapped, uint256 lmFundingEth) internal {
|
|||
|
|
(factory, pool, weth, kraiken, stake, lm,, token0isWeth) = testEnv.setupEnvironmentWithExistingFactory(factory, wethIsToken0, fees, optimizer);
|
|||
|
|
|
|||
|
|
swapExecutor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm, uncapped);
|
|||
|
|
|
|||
|
|
vm.deal(address(lm), lmFundingEth);
|
|||
|
|
vm.prank(address(lm));
|
|||
|
|
weth.deposit{ value: lmFundingEth / 2 }();
|
|||
|
|
|
|||
|
|
vm.prank(fees);
|
|||
|
|
try lm.recenter() { } catch { }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ══════════════════════════════════════════════════════════════════════
|
|||
|
|
// Background Liquidity (Gaussian competition)
|
|||
|
|
// ══════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
/// @notice Deploy background LP with Gaussian-approximating stacked positions.
|
|||
|
|
/// KRK is acquired by buying from the pool (realistic — no free minting).
|
|||
|
|
/// @param ethPerLayer ETH per layer (5 layers total). Half goes to buying KRK,
|
|||
|
|
/// half stays as WETH for the liquidity positions.
|
|||
|
|
function _deployBackgroundLP(uint256 ethPerLayer) internal {
|
|||
|
|
backgroundLP = new BackgroundLP(pool, weth, kraiken, token0isWeth);
|
|||
|
|
|
|||
|
|
// Fund with ETH (2× total: half to buy KRK, half for LP positions)
|
|||
|
|
uint256 totalEth = ethPerLayer * 5;
|
|||
|
|
vm.deal(address(backgroundLP), totalEth * 4);
|
|||
|
|
vm.prank(address(backgroundLP));
|
|||
|
|
weth.deposit{ value: totalEth * 2 }();
|
|||
|
|
|
|||
|
|
// Buy KRK from the pool — just like a real LP would.
|
|||
|
|
// Buy in small chunks with recenters to avoid overshooting thin anchors.
|
|||
|
|
address bgAddr = address(backgroundLP);
|
|||
|
|
uint256 chunkSize = totalEth / 5;
|
|||
|
|
for (uint256 i = 0; i < 5; i++) {
|
|||
|
|
uint256 buyAmount = chunkSize;
|
|||
|
|
uint256 wethBal = weth.balanceOf(bgAddr);
|
|||
|
|
if (buyAmount > wethBal / 2) buyAmount = wethBal / 2; // keep some WETH for LP
|
|||
|
|
if (buyAmount == 0) break;
|
|||
|
|
|
|||
|
|
vm.startPrank(bgAddr);
|
|||
|
|
weth.transfer(address(swapExecutor), buyAmount);
|
|||
|
|
vm.stopPrank();
|
|||
|
|
vm.prank(bgAddr);
|
|||
|
|
try swapExecutor.executeBuy(buyAmount, bgAddr) { } catch { }
|
|||
|
|
|
|||
|
|
// Recenter between chunks to rebuild positions
|
|||
|
|
vm.warp(block.timestamp + 1 hours);
|
|||
|
|
vm.roll(block.number + 1);
|
|||
|
|
vm.prank(fees);
|
|||
|
|
try lm.recenter() { } catch { }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
backgroundLP.rebalance(ethPerLayer);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// @notice Rebalance background LP positions to track current tick.
|
|||
|
|
/// Uses existing token balances — no new ETH or KRK injected.
|
|||
|
|
function _rebalanceBackgroundLP(uint256 ethPerLayer) internal {
|
|||
|
|
if (address(backgroundLP) == address(0)) return;
|
|||
|
|
backgroundLP.rebalance(ethPerLayer);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// @notice Get fee revenue collected by feeDestination
|
|||
|
|
/// @return feeWeth WETH fees, feeKrk KRK fees
|
|||
|
|
function _getFeeRevenue() internal view returns (uint256 feeWeth, uint256 feeKrk) {
|
|||
|
|
feeWeth = weth.balanceOf(fees);
|
|||
|
|
feeKrk = kraiken.balanceOf(fees);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ══════════════════════════════════════════════════════════════════════
|
|||
|
|
// Staking Setup
|
|||
|
|
// ══════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
/// @notice Pre-seed staking to simulate a given sentiment level.
|
|||
|
|
/// Buys KRK from the pool and stakes it at the given tax rate.
|
|||
|
|
/// @param stakingPct Target % of authorized stake to fill (0-100, in whole %)
|
|||
|
|
/// @param taxRateIdx Tax rate index (0-29, lower = cheaper = more bullish)
|
|||
|
|
function _seedStaking(uint256 stakingPct, uint32 taxRateIdx) internal {
|
|||
|
|
if (stakingPct == 0) return;
|
|||
|
|
|
|||
|
|
address staker = makeAddr("staker");
|
|||
|
|
|
|||
|
|
// Buy KRK for staking — use 50 ETH per attempt, loop until target met
|
|||
|
|
uint256 maxAttempts = 20;
|
|||
|
|
for (uint256 attempt = 0; attempt < maxAttempts; attempt++) {
|
|||
|
|
// Check current staking level
|
|||
|
|
uint256 currentPct = stake.getPercentageStaked() * 100 / 1e18;
|
|||
|
|
if (currentPct >= stakingPct) break;
|
|||
|
|
|
|||
|
|
// Fund staker with ETH
|
|||
|
|
vm.deal(staker, 100 ether);
|
|||
|
|
vm.prank(staker);
|
|||
|
|
weth.deposit{ value: 50 ether }();
|
|||
|
|
|
|||
|
|
// Buy KRK
|
|||
|
|
vm.startPrank(staker);
|
|||
|
|
weth.transfer(address(swapExecutor), 50 ether);
|
|||
|
|
vm.stopPrank();
|
|||
|
|
vm.prank(staker);
|
|||
|
|
try swapExecutor.executeBuy(50 ether, staker) { }
|
|||
|
|
catch {
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Recenter to rebuild positions
|
|||
|
|
vm.warp(block.timestamp + 1 hours);
|
|||
|
|
vm.roll(block.number + 1);
|
|||
|
|
vm.prank(fees);
|
|||
|
|
try lm.recenter() { } catch { }
|
|||
|
|
|
|||
|
|
// Stake all KRK
|
|||
|
|
uint256 krkBal = kraiken.balanceOf(staker);
|
|||
|
|
if (krkBal == 0) break;
|
|||
|
|
|
|||
|
|
vm.startPrank(staker);
|
|||
|
|
kraiken.approve(address(stake), krkBal);
|
|||
|
|
uint256[] memory empty = new uint256[](0);
|
|||
|
|
try stake.snatch(krkBal, staker, taxRateIdx, empty) { }
|
|||
|
|
catch {
|
|||
|
|
vm.stopPrank();
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
vm.stopPrank();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Final recenter to pick up new optimizer params
|
|||
|
|
vm.warp(block.timestamp + 1 hours);
|
|||
|
|
vm.roll(block.number + 1);
|
|||
|
|
vm.prank(fees);
|
|||
|
|
try lm.recenter() { } catch { }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ══════════════════════════════════════════════════════════════════════
|
|||
|
|
// Trade Execution
|
|||
|
|
// ══════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
/// @notice Execute a buy trade via the SwapExecutor
|
|||
|
|
/// @return success Whether the buy produced a non-zero result
|
|||
|
|
function _executeBuy(uint256 amount) internal returns (bool success) {
|
|||
|
|
if (amount == 0 || weth.balanceOf(trader) < amount) return false;
|
|||
|
|
|
|||
|
|
vm.startPrank(trader);
|
|||
|
|
weth.transfer(address(swapExecutor), amount);
|
|||
|
|
try swapExecutor.executeBuy(amount, trader) returns (uint256 actualAmount) {
|
|||
|
|
vm.stopPrank();
|
|||
|
|
_recoverStuckTokens();
|
|||
|
|
return actualAmount > 0;
|
|||
|
|
} catch {
|
|||
|
|
vm.stopPrank();
|
|||
|
|
_recoverStuckTokens();
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// @notice Execute a sell trade via the SwapExecutor
|
|||
|
|
/// @return success Whether the sell produced a non-zero result
|
|||
|
|
function _executeSell(uint256 amount) internal returns (bool success) {
|
|||
|
|
if (amount == 0 || kraiken.balanceOf(trader) < amount) return false;
|
|||
|
|
|
|||
|
|
vm.startPrank(trader);
|
|||
|
|
kraiken.transfer(address(swapExecutor), amount);
|
|||
|
|
try swapExecutor.executeSell(amount, trader) returns (uint256 actualAmount) {
|
|||
|
|
vm.stopPrank();
|
|||
|
|
_recoverStuckTokens();
|
|||
|
|
return actualAmount > 0;
|
|||
|
|
} catch {
|
|||
|
|
vm.stopPrank();
|
|||
|
|
_recoverStuckTokens();
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ══════════════════════════════════════════════════════════════════════
|
|||
|
|
// Recenter
|
|||
|
|
// ══════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
/// @notice Advance time and attempt a recenter
|
|||
|
|
function _tryRecenter() internal returns (bool) {
|
|||
|
|
vm.warp(block.timestamp + RECENTER_TIME_ADVANCE);
|
|||
|
|
vm.roll(block.number + 1);
|
|||
|
|
vm.prank(fees);
|
|||
|
|
try lm.recenter{ gas: RECENTER_GAS_LIMIT }() {
|
|||
|
|
return true;
|
|||
|
|
} catch {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ══════════════════════════════════════════════════════════════════════
|
|||
|
|
// Liquidation
|
|||
|
|
// ══════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
/// @notice Sell all trader's KRAIKEN, retrying with recenters between attempts
|
|||
|
|
function _liquidateTraderHoldings() internal {
|
|||
|
|
uint256 remaining = kraiken.balanceOf(trader);
|
|||
|
|
uint256 attempts;
|
|||
|
|
|
|||
|
|
while (remaining > 0 && attempts < LIQUIDATION_MAX_ATTEMPTS) {
|
|||
|
|
uint256 prev = remaining;
|
|||
|
|
uint256 wethBefore = weth.balanceOf(trader);
|
|||
|
|
|
|||
|
|
vm.startPrank(trader);
|
|||
|
|
kraiken.transfer(address(swapExecutor), remaining);
|
|||
|
|
vm.stopPrank();
|
|||
|
|
|
|||
|
|
try swapExecutor.executeSell(remaining, trader) {
|
|||
|
|
if (weth.balanceOf(trader) <= wethBefore) {
|
|||
|
|
_recoverStuckTokens();
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
} catch {
|
|||
|
|
_recoverStuckTokens();
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_recoverStuckTokens();
|
|||
|
|
|
|||
|
|
if (attempts % LIQUIDATION_RECENTER_INTERVAL == LIQUIDATION_RECENTER_INTERVAL - 1) {
|
|||
|
|
_tryRecenter();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
remaining = kraiken.balanceOf(trader);
|
|||
|
|
if (remaining >= prev) break;
|
|||
|
|
unchecked {
|
|||
|
|
attempts++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_recoverStuckTokens();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ══════════════════════════════════════════════════════════════════════
|
|||
|
|
// Token Recovery & Cleanup
|
|||
|
|
// ══════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
/// @notice Recover any tokens stuck in SwapExecutor back to the trader
|
|||
|
|
function _recoverStuckTokens() internal {
|
|||
|
|
uint256 stuckKraiken = kraiken.balanceOf(address(swapExecutor));
|
|||
|
|
if (stuckKraiken > 0) {
|
|||
|
|
vm.prank(address(swapExecutor));
|
|||
|
|
kraiken.transfer(trader, stuckKraiken);
|
|||
|
|
}
|
|||
|
|
uint256 stuckWeth = weth.balanceOf(address(swapExecutor));
|
|||
|
|
if (stuckWeth > 0) {
|
|||
|
|
vm.prank(address(swapExecutor));
|
|||
|
|
weth.transfer(trader, stuckWeth);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// @notice Burn all trader + SwapExecutor tokens to prevent PnL leakage between runs
|
|||
|
|
function _cleanupTraderTokens() internal {
|
|||
|
|
uint256 leftoverWeth = weth.balanceOf(trader);
|
|||
|
|
if (leftoverWeth > 0) {
|
|||
|
|
vm.prank(trader);
|
|||
|
|
weth.transfer(DEAD_ADDRESS, leftoverWeth);
|
|||
|
|
}
|
|||
|
|
uint256 leftoverKraiken = kraiken.balanceOf(trader);
|
|||
|
|
if (leftoverKraiken > 0) {
|
|||
|
|
vm.prank(trader);
|
|||
|
|
kraiken.transfer(DEAD_ADDRESS, leftoverKraiken);
|
|||
|
|
}
|
|||
|
|
uint256 stuckWeth = weth.balanceOf(address(swapExecutor));
|
|||
|
|
if (stuckWeth > 0) {
|
|||
|
|
vm.prank(address(swapExecutor));
|
|||
|
|
weth.transfer(DEAD_ADDRESS, stuckWeth);
|
|||
|
|
}
|
|||
|
|
uint256 stuckKraiken = kraiken.balanceOf(address(swapExecutor));
|
|||
|
|
if (stuckKraiken > 0) {
|
|||
|
|
vm.prank(address(swapExecutor));
|
|||
|
|
kraiken.transfer(DEAD_ADDRESS, stuckKraiken);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ══════════════════════════════════════════════════════════════════════
|
|||
|
|
// LM ETH Measurement
|
|||
|
|
// ══════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
/// @notice Approximate total ETH attributable to the LM (direct balance + pool WETH)
|
|||
|
|
/// @dev Pool WETH includes both LM position ETH and trader buy ETH in transit.
|
|||
|
|
/// This is a rough proxy — trader PnL is the reliable safety metric.
|
|||
|
|
function _getLmSystemEth() internal view returns (uint256) {
|
|||
|
|
return address(lm).balance + weth.balanceOf(address(lm)) + weth.balanceOf(address(pool));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ══════════════════════════════════════════════════════════════════════
|
|||
|
|
// CSV Parsing Utilities
|
|||
|
|
// ══════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
/// @notice Parse a comma-separated string of uint256 values
|
|||
|
|
function _parseUintArray(string memory csv) internal pure returns (uint256[] memory) {
|
|||
|
|
bytes memory b = bytes(csv);
|
|||
|
|
uint256 count = 1;
|
|||
|
|
for (uint256 i = 0; i < b.length; i++) {
|
|||
|
|
if (b[i] == ",") count++;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
uint256[] memory result = new uint256[](count);
|
|||
|
|
uint256 idx;
|
|||
|
|
uint256 start;
|
|||
|
|
|
|||
|
|
for (uint256 i = 0; i <= b.length; i++) {
|
|||
|
|
if (i == b.length || b[i] == ",") {
|
|||
|
|
uint256 num;
|
|||
|
|
for (uint256 j = start; j < i; j++) {
|
|||
|
|
require(b[j] >= "0" && b[j] <= "9", "Invalid number in CSV");
|
|||
|
|
num = num * 10 + (uint256(uint8(b[j])) - 48);
|
|||
|
|
}
|
|||
|
|
result[idx] = num;
|
|||
|
|
idx++;
|
|||
|
|
start = i + 1;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ══════════════════════════════════════════════════════════════════════
|
|||
|
|
// String Helpers
|
|||
|
|
// ══════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
/// @notice Generate a random 4-character alphanumeric ID
|
|||
|
|
function _generateScenarioId() internal view returns (string memory) {
|
|||
|
|
uint256 rand = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao)));
|
|||
|
|
bytes memory chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|||
|
|
bytes memory result = new bytes(4);
|
|||
|
|
|
|||
|
|
for (uint256 i = 0; i < 4; i++) {
|
|||
|
|
result[i] = chars[rand % chars.length];
|
|||
|
|
rand = rand / chars.length;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return string(result);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// @notice Pad a number with leading zeros
|
|||
|
|
function _padNumber(uint256 num, uint256 digits) internal pure returns (string memory) {
|
|||
|
|
string memory numStr = vm.toString(num);
|
|||
|
|
bytes memory numBytes = bytes(numStr);
|
|||
|
|
|
|||
|
|
if (numBytes.length >= digits) return numStr;
|
|||
|
|
|
|||
|
|
bytes memory result = new bytes(digits);
|
|||
|
|
uint256 padding = digits - numBytes.length;
|
|||
|
|
|
|||
|
|
for (uint256 i = 0; i < padding; i++) {
|
|||
|
|
result[i] = "0";
|
|||
|
|
}
|
|||
|
|
for (uint256 i = 0; i < numBytes.length; i++) {
|
|||
|
|
result[padding + i] = numBytes[i];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return string(result);
|
|||
|
|
}
|
|||
|
|
}
|