Uniswap v3 詳解(三):交易過程

本文從copy自https://liaoph.com/uniswap-v3-3/

交易過程

v3 的 UniswapV3Pool 提供了比較底層的交易接口毁渗,而在 SwapRouter 合約中封裝了面向用戶的交易接口:

exactInput:指定交易對路徑,付出的 x token 數(shù)和預期得到的最小 y token 數(shù)(x, y 可以互換)
exactOutput:指定交易路徑跋理,付出的 x token 最大數(shù)和預期得到的 y token 數(shù)(x, y 可以互換)
這里我們講解 exactInput 這個接口斩披,調用流程如下:


1637901419(1).png

路徑選擇

在進行兩個代幣交易時罚随,是首先需要在鏈下計算出交易的路徑兵钮,例如使用 ETH -> DAI

  • 可以直接通過 ETH/DAI 的交易池完成
  • 也可以通過 ETH -> USDC -> DAI 路徑耐版,即經(jīng)過 ETH/USDC, USDC/DAI 兩個交易池完成交易

Uniswap 的前端會幫用戶實時計算出最優(yōu)路徑(即交易的收益最高)全景,作為參數(shù)傳給合約調用。前端中這部分計算的具體實現(xiàn)在這里骨望,具體過程為先用需要交易的輸入代幣硬爆,輸出代幣,以及一系列可用的中間代幣(代碼中叫 Base token)生成所有的路徑(當然為了降低復雜度擎鸠,路徑中最多包含3個代幣)缀磕,然后遍歷每個路徑輸出的輸出代幣數(shù)量,最后選取最佳路徑劣光。

事實上因為 v3 引入了費率的原因袜蚕,在路徑選擇的過程中還需要考慮費率的因素。關于交易結果的預計算绢涡,可以參考本文[末尾處]更新的內容牲剃。

交易入口

交易的入口函數(shù)是 exactInput 函數(shù),代碼如下:

struct ExactInputParams {
    bytes path;                 // 路徑
    address recipient;          // 收款地址
    uint256 deadline;           // 交易有效期
    uint256 amountIn;           // 輸入的 token 數(shù)(輸入的 token 地址就是 path 中的第一個地址)
    uint256 amountOutMinimum;   // 預期交易最少獲得的 token 數(shù)(獲得的 token 地址就是 path 中最后一個地址)
}

function exactInput(ExactInputParams memory params)
    external
    payable
    override
    checkDeadline(params.deadline)
    returns (uint256 amountOut)
{
    // 通過循環(huán)雄可,遍歷傳入的路徑凿傅,進行交易
    while (true) {
        bool hasPools = params.path.hasPools();

        // 完成當前路徑的交易
        params.amountIn = exactInputSingle(
            params.amountIn,
            // 如果是中間交易,又合約代為收取和支付中間代幣
            hasPools ? address(this) : params.recipient,
            // 給回調函數(shù)用的參數(shù)
            SwapData({
                path: params.path.getFirstPool(),
                payer: msg.sender
            })
        );

        // 如果路徑全部遍歷完成数苫,則退出循環(huán)聪舒,交易完成
        if (hasPools) {
            // 步進 path 中的值
            params.path = params.path.skipToken();
        } else {
            amountOut = params.amountIn;
            break;
        }
    }

    // 檢查交易是否滿足預期
    require(amountOut >= params.amountOutMinimum, 'Too little received');
}

這里使用一個循環(huán)遍歷傳入的路徑,路徑中包含了交易過程中所有的 token文判,每相鄰的兩個 token 組成了一個交易對。例如當需要通過 ETH -> USDC -> DAI 路徑進行交易時室梅,會經(jīng)過兩個池:ETH/USDC 和 USDC/DAI戏仓,最終得到 DAI 代幣。如前所述亡鼠,這里其實還包含了每個交易對所選擇的費率赏殃。

路徑編碼/解碼

上面輸入的參數(shù)中 path 字段是 bytes 類型,通過這種類型可以實現(xiàn)更緊湊的編碼间涵。Uniswap 會將 bytes 作為一個數(shù)組使用仁热,bytes 類型就是一連串的 byte1,但是不會對每一個成員使用一個 word勾哩,因此相比普通數(shù)組其結構更加緊湊抗蠢。在 Uniswap V3 中, path 內部編碼結構如下圖:


1637901565(1).png

圖中展示了一個包含 2個路徑(pool0, 和 pool1)的 path 編碼思劳。Uniswap 將編碼解碼操作封裝在了 Path 庫中迅矛,本文不再贅述其過程。每次交易時潜叛,會取出頭部的 tokenIn, tokenOut, fee秽褒,使用這三個參數(shù)找到對應的交易池壶硅,完成交易。

單個池的交易過程

單個池的交易在 exactInputSingle 函數(shù)中:

function exactInputSingle(
    uint256 amountIn,
    address recipient,
    SwapData memory data
) private returns (uint256 amountOut) {
    // 將 path 解碼销斟,獲取頭部的 tokenIn, tokenOut, fee
    (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();

    // 因為交易池只保存了 token x 的價格庐椒,這里我們需要知道輸入的 token 是交易池 x token 還是 y token
    bool zeroForOne = tokenIn < tokenOut;

    // 完成交易
    (int256 amount0, int256 amount1) =
        getPool(tokenIn, tokenOut, fee).swap(
            recipient,
            zeroForOne,
            amountIn.toInt256(),
            zeroForOne ? MIN_SQRT_RATIO : MAX_SQRT_RATIO,
            // 給回調函數(shù)用的參數(shù)
            abi.encode(data)
        );

    return uint256(-(zeroForOne ? amount1 : amount0));
}
1637901645(1).png

交易分解

1637901681(1).png
...
// 將交易前的元數(shù)據(jù)保存在內存中,后續(xù)的訪問通過 `MLOAD` 完成蚂踊,節(jié)省 gas
Slot0 memory slot0Start = slot0;
...
// 防止交易過程中回調到合約中其他的函數(shù)中修改狀態(tài)變量
slot0.unlocked = false;

// 這里也是緩存交易錢的數(shù)據(jù)乏梁,節(jié)省 gas
SwapCache memory cache =
    SwapCache({
        liquidityStart: liquidity,
        blockTimestamp: _blockTimestamp(),
        feeProtocol: zeroForOne ? (slot0Start.feeProtocol % 16) : (slot0Start.feeProtocol >> 4)
    });

// 判斷是否指定了 tokenIn 的數(shù)量
bool exactInput = amountSpecified > 0;

// 保存交易過程中計算所需的中間變量,這些值在交易的步驟中可能會發(fā)生變化
SwapState memory state =
    SwapState({
        amountSpecifiedRemaining: amountSpecified,
        amountCalculated: 0,
        sqrtPriceX96: slot0Start.sqrtPriceX96,
        tick: slot0Start.tick,
        feeGrowthGlobalX128: zeroForOne ? feeGrowthGlobal0X128 : feeGrowthGlobal1X128,
        protocolFee: 0,
        liquidity: cache.liquidityStart
    });
...

上面的代碼都是交易前的準備工作照筑,實際的交易在一個循環(huán)中發(fā)生:

// 只要 tokenIn
while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) {
    // 交易過程每一次循環(huán)的狀態(tài)變量
    StepComputations memory step;

    // 交易的起始價格
    step.sqrtPriceStartX96 = state.sqrtPriceX96;

    // 通過位圖找到下一個可以選的交易價格挫掏,這里可能是下一個流動性的邊界,也可能還是在本流動性中
    (step.tickNext, step.initialized) = tickBitmap.nextInitializedTickWithinOneWord(
        state.tick,
        tickSpacing,
        zeroForOne
    );

    ...

    // 從 tick index 計算 sqrt(price)
    step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext);

    // 計算當價格到達下一個交易價格時特纤,tokenIn 是否被耗盡军俊,如果被耗盡,則交易結束捧存,還需要重新計算出 tokenIn 耗盡時的價格
    // 如果沒被耗盡粪躬,那么還需要繼續(xù)進入下一個循環(huán)
    (state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep(
        state.sqrtPriceX96,
        (zeroForOne ? step.sqrtPriceNextX96 < sqrtPriceLimitX96 : step.sqrtPriceNextX96 > sqrtPriceLimitX96)
            ? sqrtPriceLimitX96
            : step.sqrtPriceNextX96,
        state.liquidity,
        state.amountSpecifiedRemaining,
        fee
    );

    // 更新 tokenIn 的余額,以及 tokenOut 數(shù)量昔穴,注意當指定 tokenIn 的數(shù)量進行交易時镰官,這里的 tokenOut 是負數(shù)
    if (exactInput) {
        state.amountSpecifiedRemaining -= (step.amountIn + step.feeAmount).toInt256();
        state.amountCalculated = state.amountCalculated.sub(step.amountOut.toInt256());
    } else {
        state.amountSpecifiedRemaining += step.amountOut.toInt256();
        state.amountCalculated = state.amountCalculated.add((step.amountIn + step.feeAmount).toInt256());
    }

    ...

    // 按需決定是否需要更新流動性 L 的值
    if (state.sqrtPriceX96 == step.sqrtPriceNextX96) {
        // 檢查 tick index 是否為另一個流動性的邊界
        if (step.initialized) {
            int128 liquidityNet =
                ticks.cross(
                    step.tickNext,
                    (zeroForOne ? state.feeGrowthGlobalX128 : feeGrowthGlobal0X128),
                    (zeroForOne ? feeGrowthGlobal1X128 : state.feeGrowthGlobalX128)
                );
            // 根據(jù)價格增加/減少,即向左或向右移動吗货,增加/減少相應的流動性
            if (zeroForOne) liquidityNet = -liquidityNet;

            secondsOutside.cross(step.tickNext, tickSpacing, cache.blockTimestamp);

            // 更新流動性
            state.liquidity = LiquidityMath.addDelta(state.liquidity, liquidityNet);
        }

        // 在這里更 tick 的值泳唠,使得下一次循環(huán)時讓 tickBitmap 進入下一個 word 中查詢
        state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext;
    } else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) {
        // 如果 tokenIn 被耗盡,那么計算當前價格對應的 tick
        state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);
    }
}

上面的代碼即交易的主循環(huán)宙搬,實現(xiàn)思路即以一個 tickBitmap 的 word 為最大單位笨腥,在此單位內計算相同流動性區(qū)間的交易數(shù)值,如果交易沒有完成勇垛,那么更新流動性的值脖母,進入下一個流動性區(qū)間計算,如果 tick index 移動到 word 的邊界闲孤,那么步進到下一個 word.

關于 tickBitmap 中下一個可用價格 tick index 的查找谆级,在函數(shù) TickBitmap 中實現(xiàn),這里不做詳細描述讼积。

拆分后的交易計算

交易是否能夠結束的關鍵計算在 SwapMath.computeSwapStep 中完成肥照,這里計算了交易是否能在目標價格范圍內結束,以及消耗的 tokenIn 和得到的 tokenOut. 這里摘取此函數(shù)部分代碼進行分析(這里僅摘取 exactIn 時的代碼):

function computeSwapStep(
    uint160 sqrtRatioCurrentX96,
    uint160 sqrtRatioTargetX96,
    uint128 liquidity,
    int256 amountRemaining,
    uint24 feePips
)
    internal
    pure
    returns (
        uint160 sqrtRatioNextX96,
        uint256 amountIn,
        uint256 amountOut,
        uint256 feeAmount
    )
{
    // 判斷交易的方向勤众,即價格降低或升高
    bool zeroForOne = sqrtRatioCurrentX96 >= sqrtRatioTargetX96;
    // 判斷是否指定了精確的 tokenIn 數(shù)量
    bool exactIn = amountRemaining >= 0;
    ...

函數(shù)的輸入?yún)?shù)是當前價格建峭,目標價格,當前的流動性决摧,以及 tokenIn 的余額亿蒸。

if (exactIn) {
    // 先將 tokenIn 的余額扣除掉最大所需的手續(xù)費
    uint256 amountRemainingLessFee = FullMath.mulDiv(uint256(amountRemaining), 1e6 - feePips, 1e6);
    // 通過公式計算出到達目標價所需要的 tokenIn 數(shù)量凑兰,這里對 x token 和 y token 計算的公式是不一樣的
    amountIn = zeroForOne
        ? SqrtPriceMath.getAmount0Delta(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, true)
        : SqrtPriceMath.getAmount1Delta(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, true);
    // 判斷余額是否充足,如果充足边锁,那么這次交易可以到達目標交易價格姑食,否則需要計算出當前 tokenIn 能到達的目標交易價
    if (amountRemainingLessFee >= amountIn) sqrtRatioNextX96 = sqrtRatioTargetX96;
    else
        // 當余額不充足的時候計算能夠到達的目標交易價
        sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromInput(
            sqrtRatioCurrentX96,
            liquidity,
            amountRemainingLessFee,
            zeroForOne
        );
} else {
    ...
}
1637901818(1).png
// 判斷是否能夠到達目標價
bool max = sqrtRatioTargetX96 == sqrtRatioNextX96;

// get the input/output amounts
if (zeroForOne) {
    // 根據(jù)是否到達目標價格,計算 amountIn/amountOut 的值
    amountIn = max && exactIn
        ? amountIn
        : SqrtPriceMath.getAmount0Delta(sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, true);
    amountOut = max && !exactIn
        ? amountOut
        : SqrtPriceMath.getAmount1Delta(sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, false);
} else {
    ...
}

// 這里對 Output 進行 cap 是因為前面在計算 amountOut 時茅坛,有可能會使用 sqrtRatioNextX96 來進行計算音半,而 sqrtRatioNextX96
// 可能被 Round 之后導致 sqrt_P 偏大,從而導致計算的 amountOut 偏大
if (!exactIn && amountOut > uint256(-amountRemaining)) {
    amountOut = uint256(-amountRemaining);
}

if (exactIn && sqrtRatioNextX96 != sqrtRatioTargetX96) {
    // 如果沒能到達目標價贡蓖,即交易結束曹鸠,剩余的 tokenIn 將全部作為手續(xù)費
    // 為了不讓計算進一步復雜化,這里直接將剩余的 tokenIn 將全部作為手續(xù)費
    // 因此會多收取一部分手續(xù)費斥铺,即按本次交易的最大手續(xù)費收取
    feeAmount = uint256(amountRemaining) - amountIn;
} else {
    feeAmount = FullMath.mulDivRoundingUp(amountIn, feePips, 1e6 - feePips);
}
1637901862(1).png

交易收尾階段

我們再回到 swap 函數(shù)中循環(huán)檢查條件:

while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) {
    ...
}
1637901934(1).png
// 確定最終用戶支付的 token 數(shù)和得到的 token 數(shù)
(amount0, amount1) = zeroForOne == exactInput
    ? (amountSpecified - state.amountSpecifiedRemaining, state.amountCalculated)
    : (state.amountCalculated, amountSpecified - state.amountSpecifiedRemaining);

// 扣除用戶需要支付的 token
if (zeroForOne) {
    // 將 tokenOut 支付給用戶彻桃,前面說過 tokenOut 記錄的是負數(shù)
    if (amount1 < 0) TransferHelper.safeTransfer(token1, recipient, uint256(-amount1));

    uint256 balance0Before = balance0();
    // 還是通過回調的方式,扣除用戶需要支持的 token
    IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);
    // 校驗扣除是否成功
    require(balance0Before.add(uint256(amount0)) <= balance0(), 'IIA');
} else {
    ...
}

// 記錄日志
emit Swap(msg.sender, recipient, amount0, amount1, state.sqrtPriceX96, state.tick);
// 解除防止重入的鎖
slot0.unlocked = true;
}

這里還是通過回調完成用戶支付 token 的費用晾蜘。因為發(fā)送用戶 token 是在回調函數(shù)之前完成的邻眷,因此這個 swap 函數(shù)是可以被當作 flash swap 來使用的。

需要注意剔交,如果本次交易是交易路徑中的一次中間交易肆饶,那么扣除的 token 是從 SwapRouter 中扣除的,交易完成獲得的 token 也會發(fā)送給 SwapRouter 以便其進行下一步的交易岖常,我們回到 SwapRouter 中的 exactInput 函數(shù):

params.amountIn = exactInputSingle(
    params.amountIn,
    // 這里會判斷是否是最后一次交易驯镊,當是最后一次交易時,獲取的 token 的地址才是用戶的指定的地址
    hasPools ? address(this) : params.recipient,
    SwapData({
        path: params.path.getFirstPool(),
        payer: msg.sender
    })
);

再來看一下支付的回調函數(shù):

function uniswapV3SwapCallback(
    int256 amount0Delta,
    int256 amount1Delta,
    bytes calldata _data
) external override {
    SwapData memory data = abi.decode(_data, (SwapData));
    (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();
    CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);

    // 這里有點繞竭鞍,目的就是判斷函數(shù)的參數(shù)中哪個是本次支付需要支付的代幣
    (bool isExactInput, uint256 amountToPay) =
        amount0Delta > 0
            ? (tokenIn < tokenOut, uint256(amount0Delta))
            : (tokenOut < tokenIn, uint256(amount1Delta));
    if (isExactInput) {
        // 調用 pay 函數(shù)支付代幣
        pay(tokenIn, data.payer, msg.sender, amountToPay);
    } else {
        ...
    }
}

回調完成后板惑,swap 函數(shù)會返回本次交易得到的代幣數(shù)量。exactInput 將判斷是否進行下一個路徑的交易笼蛛,直至所有的交易完成洒放,進行輸入約束的檢查:

require(amountOut >= params.amountOutMinimum, 'Too little received');

如果交易的獲得 token 數(shù)滿足約束蛉鹿,則本次交易結束滨砍。

本文僅對 exactInput 這一種交易情況進行了分析,理解了這個交易的整個流程后妖异,就可以觸類旁通理解 exactOutput 的交易過程惋戏。

交易預計算

當用戶和 uniswap 前端進行交互時,前端需要預先計算出用戶輸入 token 能夠預期得到的 token 數(shù)量他膳。

這個功能在 uniswap v2 有非常簡單的實現(xiàn)响逢,只需要查詢處合約中兩個代幣的余額就可以完成預計算。

但是在 v3 版本中棕孙,由于交易的計算需要使用合約內的 tick 信息舔亭,預計算只能由 uniswap v3 pool 合約來完成些膨,但是 pool 合約中的計算函數(shù)都是會更改合約狀態(tài)的 external 函數(shù),那么如何把這個函數(shù)當作 view/pure 函數(shù)來使用呢钦铺?uniswap v3 periphery 倉庫中給出了一個非常 tricky 的實現(xiàn)订雾,代碼在 contracts/lens/Quoter.sol 中:

function quoteExactInputSingle(
    address tokenIn,
    address tokenOut,
    uint24 fee,
    uint256 amountIn,
    uint160 sqrtPriceLimitX96
) public override returns (uint256 amountOut) {
    bool zeroForOne = tokenIn < tokenOut;

    try
        getPool(tokenIn, tokenOut, fee).swap(  // 調用 pool 合約的 swap 接口來模擬一次真實的交易
            address(this), // address(0) might cause issues with some tokens
            zeroForOne,
            amountIn.toInt256(),
            sqrtPriceLimitX96 == 0
                ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
                : sqrtPriceLimitX96,
            abi.encodePacked(tokenIn, fee, tokenOut)
        )
    {} catch (bytes memory reason) {
        return parseRevertReason(reason);
    }
}

可以看到函數(shù)中調用了 getPool(tokenIn, tokenOut, fee).swap(),即 pool 合約的真實交易函數(shù)矛洞,但是實際上我們并不想讓交易發(fā)生洼哎,這個交易調用必定也會失敗,因此合約使用了 try/catch 的方式捕獲錯誤沼本,并且在回調函數(shù)中獲取到模擬交易的結果噩峦,存入內存中。

可以看回調函數(shù):

function uniswapV3SwapCallback(
    int256 amount0Delta,
    int256 amount1Delta,
    bytes memory path
) external view override {
    require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported
    (address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool();
    CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);

    (bool isExactInput, uint256 amountToPay, uint256 amountReceived) =
        amount0Delta > 0
            ? (tokenIn < tokenOut, uint256(amount0Delta), uint256(-amount1Delta))
            : (tokenOut < tokenIn, uint256(amount1Delta), uint256(-amount0Delta));
    if (isExactInput) {
        assembly {  // 這里代碼需要將結果保存在內存中
            let ptr := mload(0x40)  // 0x40 是 solidity 定義的 free memory pointer
            mstore(ptr, amountReceived) // 將結果保存起來
            revert(ptr, 32) // revert 掉交易抽兆,并將內存中的數(shù)據(jù)作為 revert data
        }
    } else {
        // if the cache has been populated, ensure that the full output amount has been received
        if (amountOutCached != 0) require(amountReceived == amountOutCached);
        assembly {
            let ptr := mload(0x40)
            mstore(ptr, amountToPay)
            revert(ptr, 32)
        }
    }
}

這個回調函數(shù)主要的作用就是將 swap() 函數(shù)計算處的結果保存到內存中识补,這里使用了 assembly 來訪問 solidity 的 free memory pointer,關于 solidity 內存布局郊丛,可以參考文檔:Layout in Memory.

將結果保存到內存中時候就將交易 revert 掉李请,然后在 quoteExactInputSingle 中捕獲這個錯誤,并將內存中的信息讀取出來厉熟,返回給調用者:

/// @dev Parses a revert reason that should contain the numeric quote
function parseRevertReason(bytes memory reason) private pure returns (uint256) {
    if (reason.length != 32) { // swap 函數(shù)正常 revert 的情況
        if (reason.length < 68) revert('Unexpected error');
        assembly {
            reason := add(reason, 0x04)
        }
        revert(abi.decode(reason, (string)));
    }
    return abi.decode(reason, (uint256)); // 這里捕獲前面回調函數(shù)保存在內存中的結果导盅。
}

總結:通過 try/catch 結合回調函數(shù),模擬計算結果揍瑟,實現(xiàn)了交易預計算的功能白翻,這樣 uniswap 前端就能夠在獲取用戶輸入后進行交易的預計算了,這部分前端的實現(xiàn)在這里绢片。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末滤馍,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子底循,更是在濱河造成了極大的恐慌巢株,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件熙涤,死亡現(xiàn)場離奇詭異阁苞,居然都是意外死亡,警方通過查閱死者的電腦和手機祠挫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門那槽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人等舔,你說我怎么就攤上這事骚灸。” “怎么了慌植?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵甚牲,是天一觀的道長义郑。 經(jīng)常有香客問我,道長丈钙,這世上最難降的妖魔是什么魔慷? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮著恩,結果婚禮上院尔,老公的妹妹穿的比我還像新娘。我一直安慰自己喉誊,他們只是感情好邀摆,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著伍茄,像睡著了一般栋盹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上敷矫,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天例获,我揣著相機與錄音,去河邊找鬼曹仗。 笑死榨汤,一個胖子當著我的面吹牛,可吹牛的內容都是我干的怎茫。 我是一名探鬼主播收壕,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼轨蛤!你這毒婦竟也來了蜜宪?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤祥山,失蹤者是張志新(化名)和其女友劉穎圃验,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缝呕,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡澳窑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了岳颇。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片照捡。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡颅湘,死狀恐怖话侧,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情闯参,我是刑警寧澤瞻鹏,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布悲立,位于F島的核電站,受9級特大地震影響新博,放射性物質發(fā)生泄漏薪夕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一赫悄、第九天 我趴在偏房一處隱蔽的房頂上張望原献。 院中可真熱鬧,春花似錦埂淮、人聲如沸姑隅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽讲仰。三九已至,卻和暖如春痪蝇,著一層夾襖步出監(jiān)牢的瞬間鄙陡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工躏啰, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留趁矾,地道東北人。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓给僵,卻偏偏與公主長得像愈魏,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子想际,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內容