2023.07.10

Uniswap v4 プロトコルをソースコードから理解する

こんにちは。次世代システム研究室のT.M です。

注意

本稿は2023/6/30 時点の情報です。情報が更新されていることがありますので、ご注意ください。特にUniswap v4 プロトコルは現在もまだドラフトであり、仕様が変更になる可能性が大いにあります。

Uniswap はUniswap Labs. の商標です。

はじめに

Uniswap は2018年にローンチし、現在、最も取引量が多いDEX です。現時点でv1、v2、v3 と3つのバージョンが公開されており、2023年6月にv4 の構想が発表されました。その構想発表時に公開されたホワイトペーパーでは、Hooks、Singleton、Flash Accounting という3つの機能が記載されていました。本稿では、それらの3つの機能についてホワイトペーパーとソースコードから説明していきます。

Hooks

流動性プールにはいくつかのライフサイクルイベントがあります。v3 では、それらのイベントが厳密な順序で実行をされていました。v4 では、Hooks という機能を導入することで、ポジションの変更前後やスワップの前後などの重要なライフサイクルイベントにおいて、任意のロジックを実行することができるようになります。これにより流動性とセキュリティを向上させることができます。

IHooks を実装することで、Hooks のコントラクトを作成することができます。以下はIHooks.sol のソースコードです。initialize、modifyPosition、swap、donate の4つのライフサイクルにそれぞれbefore とafter のHook が用意されています。

 
interface IHooks {
    function beforeInitialize(address sender, IPoolManager.PoolKey calldata key, uint160 sqrtPriceX96)
        external
        returns (bytes4);

    function afterInitialize(address sender, IPoolManager.PoolKey calldata key, uint160 sqrtPriceX96, int24 tick)
        external
        returns (bytes4);

    function beforeModifyPosition(
        address sender,
        IPoolManager.PoolKey calldata key,
        IPoolManager.ModifyPositionParams calldata params
    ) external returns (bytes4);

    function afterModifyPosition(
        address sender,
        IPoolManager.PoolKey calldata key,
        IPoolManager.ModifyPositionParams calldata params,
        BalanceDelta delta
    ) external returns (bytes4);

    function beforeSwap(address sender, IPoolManager.PoolKey calldata key, IPoolManager.SwapParams calldata params)
        external
        returns (bytes4);

    function afterSwap(
        address sender,
        IPoolManager.PoolKey calldata key,
        IPoolManager.SwapParams calldata params,
        BalanceDelta delta
    ) external returns (bytes4);

    function beforeDonate(address sender, IPoolManager.PoolKey calldata key, uint256 amount0, uint256 amount1)
        external
        returns (bytes4);

    function afterDonate(address sender, IPoolManager.PoolKey calldata key, uint256 amount0, uint256 amount1)
        external
        returns (bytes4);
}
Hook がどのように実行されるか、modifyPosition を例に確認していきます。プールを管理するPoolManager というコントラクトにポジションを変更するmodifyPosition という関数があります。以下のコードは、modifyPosition のコードになります。modifyPosition が実行されると、まずbeforeModifyPosition というHook を実行します。その後、下記コードでは省略していますが、ポジションを変更する処理がされます。最後に、afterModifyPosition というHook を実行します。

 
function modifyPosition(PoolKey memory key, IPoolManager.ModifyPositionParams memory params)
    external
    override
    noDelegateCall
    onlyByLocker
    returns (BalanceDelta delta)
{
    if (key.hooks.shouldCallBeforeModifyPosition()) {
        if (key.hooks.beforeModifyPosition(msg.sender, key, params) != IHooks.beforeModifyPosition.selector) {
            revert Hooks.InvalidHookResponse();
        }
    }

    ...

    if (key.hooks.shouldCallAfterModifyPosition()) {
        if (key.hooks.afterModifyPosition(msg.sender, key, params, delta) != IHooks.afterModifyPosition.selector) {
            revert Hooks.InvalidHookResponse();
        }
    }

    emit ModifyPosition(id, msg.sender, params.tickLower, params.tickUpper, params.liquidityDelta);
}

Singleton

v3 では、プールを作成するためにはコントラクトをデプロイする必要があります。そのため、プール作成にガスコストが必要です。v4 では、PoolManager と呼ばれる一つのコントラクトに全てのプールを保持することで、プール作成のガスコストを減らすことができます。

以下はPoolManager.sol のソースコードです。pools というstate があり、プールの状態を保持しています。swap において、指定したプールのswap を実行します。

 
contract PoolManager is IPoolManager, Owned, NoDelegateCall, ERC1155, IERC1155Receiver {
    ...

    mapping(PoolId id => Pool.State) public pools;

    ...

    function swap(PoolKey memory key, IPoolManager.SwapParams memory params)
        external
        override
        noDelegateCall
        onlyByLocker
        returns (BalanceDelta delta)
    {
        ...

        uint256 feeForProtocol;
        uint256 feeForHook;
        Pool.SwapState memory state;
        PoolId id = key.toId();
        (delta, feeForProtocol, feeForHook, state) = pools[id].swap(
            Pool.SwapParams({
                fee: totalSwapFee,
                tickSpacing: key.tickSpacing,
                zeroForOne: params.zeroForOne,
                amountSpecified: params.amountSpecified,
                sqrtPriceLimitX96: params.sqrtPriceLimitX96
            })
        );

        _accountPoolBalanceDelta(key, delta);

        ...
    }
}

Flash Accounting

v3 では、複数プールを跨いでのスワップにおいて、プール間のトークンの転送が必要です。そのため、複数プールを跨ぐスワップにおいて、ガスコストが掛かっていました。v4 では、PoolManager において、各プールやユーザーの残高を管理することで、複数プールを跨ぐスワップのガスコストを削減することができます。

PoolManager ではロックを獲得することで、プールおよびユーザーの残高の変化をPoolManager のstate で管理することができます。swap を実行した際は、_accountPoolBalanceDelta が実行され、lockState の各トークンの残高が変動することでスワップを行います。

 
contract PoolManager is IPoolManager, Owned, NoDelegateCall, ERC1155, IERC1155Receiver {
    ...

    address[] public override lockedBy;

    struct LockState {
        uint256 nonzeroDeltaCount;
        mapping(Currency currency => int256) currencyDelta;
    }

    mapping(uint256 index => LockState) private lockStates;
    
    ...

    function lock(bytes calldata data) external override returns (bytes memory result) {
        uint256 id = lockedBy.length;
        lockedBy.push(msg.sender);

        result = ILockCallback(msg.sender).lockAcquired(id, data);

        unchecked {
            LockState storage lockState = lockStates[id];
            if (lockState.nonzeroDeltaCount != 0) revert CurrencyNotSettled();
        }

        lockedBy.pop();
    }

    function _accountDelta(Currency currency, int128 delta) internal {
        if (delta == 0) return;

        LockState storage lockState = lockStates[lockedBy.length - 1];
        int256 current = lockState.currencyDelta[currency];

        int256 next = current + delta;
        unchecked {
            if (next == 0) {
                lockState.nonzeroDeltaCount--;
            } else if (current == 0) {
                lockState.nonzeroDeltaCount++;
            }
        }

        lockState.currencyDelta[currency] = next;
    }

    function _accountPoolBalanceDelta(PoolKey memory key, BalanceDelta delta) internal {
        _accountDelta(key.currency0, delta.amount0());
        _accountDelta(key.currency1, delta.amount1());
    }

    ...

}
ロックを獲得して、スワップを行うコードは以下のようになります。PoolManager のlock を呼び出すことで、PoolManager のstate がロックされ、呼び出し元のlockAcquired が呼び出されます。lockAcquired 内で、swap を呼び出し、スワップを実行します。

 
contract MyContract is ILockCallback {
    IPoolManager poolManager;

    function doSomethingWithPools() {
        poolManager.lock(...);
    }

    function lockAcquired(uint256 id, bytes calldata data) external returns (bytes memory) {
        poolManager.swap(...)
    }
}

まとめ

Uniswap の新しいプロトコルv4 で追加される機能である、Hooks、Singleton、Flash Accounting について、ソースコードを交えて説明しました。これらの機能により、より柔軟な取引をすることができ、また、ガスコストを大きく削減することができます。このほかにも、ETH を扱えるようにするなどの提案がされています。

 

 

次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。インフラ設計、構築経験者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。

皆さんのご応募をお待ちしています。

 

 

参考

https://blog.uniswap.org/uniswap-v4

https://github.com/Uniswap/v4-core

  • Twitter
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事