CenturionDEX
Launch App

The Flash Callback

Last modified:

Security Warning The example contract inherits from PeripheryPayments, which exposes a public sweepToken function that allows anyone to withdraw ERC-20 tokens held by the contract. Do not leave token balances in the contract between transactions. Ensure all fund movements (deposit, swap, repay, profit withdrawal) happen atomically within the flash callback so that no tokens remain in the contract after execution. In a production contract, consider overriding sweepToken with access control or removing it entirely.

Setting up the callback

Declare and override centurionV3FlashCallback. The pool calls this function after transferring the borrowed tokens, passing the fee amounts owed and the opaque data blob we encoded in initFlash.

    function centurionV3FlashCallback(
        uint256 fee0,
        uint256 fee1,
        bytes calldata data
    ) external override {

Decode the callback data back into a FlashCallbackData struct:

        FlashCallbackData memory decoded = abi.decode(data, (FlashCallbackData));

Validate that the caller is the genuine V3 pool. Without this check, any EOA could invoke the callback directly and manipulate the contract.

        CallbackValidation.verifyCallback(factory, decoded.poolKey);

Cache the token addresses and approve the router to spend the borrowed amounts:

        address token0 = decoded.poolKey.token0;
        address token1 = decoded.poolKey.token1;
 
        TransferHelper.safeApprove(token0, address(swapRouter), decoded.amount0);
        TransferHelper.safeApprove(token1, address(swapRouter), decoded.amount1);

Compute minimum acceptable outputs. Each swap must return at least the borrowed amount plus its fee; otherwise the transaction reverts — there is no profit.

        uint256 amount1Min = LowGasSafeMath.add(decoded.amount1, fee1);
        uint256 amount0Min = LowGasSafeMath.add(decoded.amount0, fee0);

Executing the swaps

Call exactInputSingle on the router twice — once per borrowed token — each time targeting a pool with a different fee tier.

Two parameters are new relative to earlier guides:

The first swap takes the borrowed amount1, routes it through the poolFee2 pool, and requires at least amount0Min of token0 back:

uint256 amountOut0 =
            swapRouter.exactInputSingle(
                ISwapRouter.ExactInputSingleParams({
                    tokenIn: token1,
                    tokenOut: token0,
                    fee: decoded.poolFee2,
                    recipient: address(this),
                    deadline: block.timestamp + 200,
                    amountIn: decoded.amount1,
                    amountOutMinimum: amount0Min,
                    sqrtPriceLimitX96: 0
                })
            );

The second swap takes the borrowed amount0, routes it through the poolFee3 pool, and requires at least amount1Min of token1 back:

uint256 amountOut1 =
            swapRouter.exactInputSingle(
                ISwapRouter.ExactInputSingleParams({
                    tokenIn: token0,
                    tokenOut: token1,
                    fee: decoded.poolFee3,
                    recipient: address(this),
                    deadline: block.timestamp + 200,
                    amountIn: decoded.amount0,
                    amountOutMinimum: amount1Min,
                    sqrtPriceLimitX96: 0
                })
            );

Repaying the pool

Calculate the total debt (principal + fee) for each token and approve the contract itself to transfer them back via pay:

uint256 amount0Owed = LowGasSafeMath.add(decoded.amount0, fee0);
uint256 amount1Owed = LowGasSafeMath.add(decoded.amount1, fee1);
 
TransferHelper.safeApprove(token0, address(this), amount0Owed);
TransferHelper.safeApprove(token1, address(this), amount1Owed);

Repay the pool. The callback is invoked by the pool itself, so msg.sender is the pool address — exactly where the tokens need to go. pay is an internal helper inherited from PeripheryPayments.

if (amount0Owed > 0) pay(token0, address(this), msg.sender, amount0Owed);
if (amount1Owed > 0) pay(token1, address(this), msg.sender, amount1Owed);

If either swap produced more than the debt, send the surplus to decoded.payer — the original msg.sender of initFlash.

    if (amountOut0 > amount0Owed) {
            uint256 profit0 = LowGasSafeMath.sub(amountOut0, amount0Owed);
 
            TransferHelper.safeApprove(token0, address(this), profit0);
            pay(token0, address(this), decoded.payer, profit0);
        }
 
    if (amountOut1 > amount1Owed) {
            uint256 profit1 = LowGasSafeMath.sub(amountOut1, amount1Owed);
            TransferHelper.safeApprove(token0, address(this), profit1);
            pay(token1, address(this), decoded.payer, profit1);
        }

Full function

    function centurionV3FlashCallback(
        uint256 fee0,
        uint256 fee1,
        bytes calldata data
    ) external override {
        FlashCallbackData memory decoded = abi.decode(data, (FlashCallbackData));
        CallbackValidation.verifyCallback(factory, decoded.poolKey);
 
        address token0 = decoded.poolKey.token0;
        address token1 = decoded.poolKey.token1;
 
        TransferHelper.safeApprove(token0, address(swapRouter), decoded.amount0);
        TransferHelper.safeApprove(token1, address(swapRouter), decoded.amount1);
 
        // profitable check
        // exactInputSingle will fail if this amount not met
        uint256 amount1Min = LowGasSafeMath.add(decoded.amount1, fee1);
        uint256 amount0Min = LowGasSafeMath.add(decoded.amount0, fee0);
 
        // call exactInputSingle for swapping token1 for token0 in pool w/fee2
        uint256 amountOut0 =
            swapRouter.exactInputSingle(
                ISwapRouter.ExactInputSingleParams({
                    tokenIn: token1,
                    tokenOut: token0,
                    fee: decoded.poolFee2,
                    recipient: address(this),
                    deadline: block.timestamp + 200,
                    amountIn: decoded.amount1,
                    amountOutMinimum: amount0Min,
                    sqrtPriceLimitX96: 0
                })
            );
 
        // call exactInputSingle for swapping token0 for token 1 in pool w/fee3
        uint256 amountOut1 =
            swapRouter.exactInputSingle(
                ISwapRouter.ExactInputSingleParams({
                    tokenIn: token0,
                    tokenOut: token1,
                    fee: decoded.poolFee3,
                    recipient: address(this),
                    deadline: block.timestamp + 200,
                    amountIn: decoded.amount0,
                    amountOutMinimum: amount1Min,
                    sqrtPriceLimitX96: 0
                })
            );
 
        // end up with amountOut0 of token0 from first swap and amountOut1 of token1 from second swap
        uint256 amount0Owed = LowGasSafeMath.add(decoded.amount0, fee0);
        uint256 amount1Owed = LowGasSafeMath.add(decoded.amount1, fee1);
 
        TransferHelper.safeApprove(token0, address(this), amount0Owed);
        TransferHelper.safeApprove(token1, address(this), amount1Owed);
 
        if (amount0Owed > 0) pay(token0, address(this), msg.sender, amount0Owed);
        if (amount1Owed > 0) pay(token1, address(this), msg.sender, amount1Owed);
 
        // if profitable pay profits to payer
        if (amountOut0 > amount0Owed) {
            uint256 profit0 = LowGasSafeMath.sub(amountOut0, amount0Owed);
 
            TransferHelper.safeApprove(token0, address(this), profit0);
            pay(token0, address(this), decoded.payer, profit0);
        }
        if (amountOut1 > amount1Owed) {
            uint256 profit1 = LowGasSafeMath.sub(amountOut1, amount1Owed);
            TransferHelper.safeApprove(token0, address(this), profit1);
            pay(token1, address(this), decoded.payer, profit1);
        }
    }