API3 DAO Revenue Incinerator

OSS Technologies Limited has deployed API3DAORevenueIncinerator.sol at Ethereum mainnet address 0x41d437332798b32092E6371B667A96779Dc623F6. It is verified, and is a slightly changed and optimised version of this code: github[dot]com/ErichDylus/API3/blob/main/contracts/SwapUSDCAndBurnAPI3.sol, an automatic DAO revenue buy and LP and burner as explained in this medium article: medium[dot]com/@ugurmersin/monetizing-data-feeds-951cd5c912bd.

We want only to contribute to the API3 DAO as a community member for now, though we also consider this an initial reference for any future API3 DAO proposals we might make. OSS Technologies Limited provides review and support services for open source software developers with a focus on Solidity; while we do not provide audits, this code has been peer reviewed, passed MythX security scans, and is not meant to hold large values (small risk at any time). Because this contract is immutable, we as deployer have no way to influence this contract and API3 is free to use it, ignore it, or alter and re-deploy it, as it is open source.

This is gas-optimised and designed for high volume low value amounts to lower MEV risks. Liquidity will be lowest at the beginning, so smaller initial amounts of USDC are advisable. A substantial ETH-API3 token value balance mismatch is unlikely because neither asset is held by this contract longer than one transaction; only USDC and LP tokens may sit idle in balance.

This contract automatically buys-and-burns API3 tokens with any ETH sent directly to the contract, so excess ETH revenue could be directed to it. This also provides a swap and burn mechanism if the ETH-USDC pair on UniV2 were to become unfavourable, or if protocol contracts were to automatically direct excess revenue in the form of ETH to this contract (for example to avoid USDC blacklisting risk).

There are events in this contract for any trackers, along with the swap events in the Uniswap version 2 router for amount of API3 tokens purchased, and the burn events in the API3 token contract.

To sum, this contract is used as follows:

  • send excess USDC revenue to the contract address at which point it is functionally burned (as it is irretrievable)
  • any address calls either (A) swapUSDCToAPI3AndLpWithETHPair() or (B) swapFractionUSDCToAPI3AndLpWithETHPair(uint256 _divisor) to swap and LP either (A) the total USDC or (B) the total USDC / _divisor, respectively
  • LP tokens can be redeemed by any address’s call to this contract only after a year, at which point the redeemed API3 tokens are burned, and the redeemed ETH and fees are automatically used to buy API3 tokens which are burned
  • if ETH is sent directly to the contract address, it automatically buys and burns API3 tokens by the receive() function

Example transactions:

  • swapFractionUSDCToAPI3AndLpWithETHPair() (_divisor of 4) tx: 0xac94a5c30e8e1d3c244b04743cb0cec0b25232a8c84060b1a8a84775ca63257b
  • swapUSDCToAPI3AndLpWithETHPair()tx: 0xab99c7014593081f4e93341002fdf75f95f9f02664e1ba8568686561c45caf19
  • 0.0333 ETH transfer (receive()) tx: 0x05e2d2c5f2cb951d1a58cb3c09a755e0a347d8acacf0b9309c899ae892e94c89)

Changes from source file: add swapFractionUSDCToAPI3AndLpWithETHPair(), use Sol-DAO FixedPointMathLib.sol library in lieu of Sol-DAO FixedPointMath.sol, YEAR_IN_SECONDS as constant variable.

⟁⟁⟁

8 Likes

Edited to remove hyperlinks.

Fantastic to see the community building.

Thanks to all contributors. The whitepaper doesn’t detail how exactly this was supposed to be done, and this is a sensible way that addresses the practical issues.

is not meant to hold large values (small risk at any time)

This is arguably not correct, the amount being held will functionally accumulate through the lifetime of the LP. For example, consider the case where it accidentally allowed the redeeming of the LP tokens after 10 years instead of 1 year as a result of a typo.

Somewhat related to this, CTT will have this audited independently because it’s very likely that someone will use this one way or another and we want to avoid potential damages as a result of that.

2 Likes

Just wanted to add that I think the changes to the source are beneficial – specifically I think swapFractionUSDCToAPI3AndLpWithETHPair() is a smart addition, in case larger-than-anticipated chunks of USDC find their way into the address at any given time; this function allows partial treatment of the USDC to mitigate slippage and MEV risk on bulk conversions.

Also glad to hear CTT will submit this for independent audit for the benefit of some additional review :+1:

If a large batch of USDC gets sent to the contract, the party that wants to capture the MEV can call swapUSDCToAPI3AndLpWithETHPair() themselves so I don’t see how swapFractionUSDCToAPI3AndLpWithETHPair() helps.

Thank you for this feedback. With this feedback and the added context that this code will be separately audited, we have made a few alterations to the API3DAORevenueIncinerator.sol file. Because we cannot attach a .txt or .sol, we can paste the entire file below.

We removed the SolDAO FixedPointMathLib library of optimised division function and reverted to normal in-line operands. The original implementation was intended for gas optimisation, but understanding this complicates external audits (especially considering very few of the library functions were actually used in the code), we eschewed the library import for the normal * and / operands. swapFractionUSDCToAPI3AndLpWithETHPair() has been retained to allow lower-slippage calls and user options.

One of our developers noted that we could also save even more gas and reduce contract size by (1) updating the compiler version to 0.8.12 and (2) changing the visibility of the constant state variables (API3_TOKEN_ADDR, LP_TOKEN_ADDR, UNI_ROUTER_ADDR, USDC_TOKEN_ADDR, WETH_TOKEN_ADDR, and YEAR_IN_SECONDS) to β€œinternal” rather than public. Because these constant variables are verifiable once the contract itself is deployed and verified, and they are of separately-verifiable contract addresses and a constant uint256 of the number of seconds in a year, there is no substantial difference in user experience or security, but a noticeable difference in contract size/gas. In fact, the new contract should save ~ 76087 gas and 283 bytes of contract size.

Should this updated version be preferable to the deployed contract, we would be happy to deploy and verify the code below following the audit. Please excuse any formatting misalignment from the pasting into this forum.

//SPDX-License-Identifier: MIT

/***** 
 ***** this code and all instances and deployments of this code are provided as-is;
 ***** no guarantee, representation nor warranty (express or implied) is given as to the safety or any attribute of the code or smart contracts
 ***** users should proceed with caution and at their own risk.

               /\   
              /__\ 
             /\  /\    
            /__\/__\
           /\  /\  /\   
          /__\/__\/__\ 
         /\  /\  /\  /\ 
        /__\/__\/__\/__\
       /\  /\  /\  /\  /\ 
      /__\/__\/  \/__\/__\ 
     /\  /\  /    \  /\  /\
    /__\/__\/______\/__\/__\
   /\  /\  /\  /\  /\  /\  /\  
  /__\/__\/__\/__\/__\/__\/__\ 
 /\  /\  /\  /\  /\  /\  /\  /\
/__\/__\/__\/__\/__\/__\/__\/__\

 
⟁ ⟁ ⟁ ⟁ ⟁ ⟁ ⟁ ⟁ ⟁ ⟁ ⟁ ⟁ 

   API3DAO REVENUE INCINERATOR 

⟁ ⟁ ⟁ ⟁ ⟁ ⟁ ⟁ ⟁ ⟁ ⟁ ⟁ ⟁ 


*/

pragma solidity >=0.8.12;

/// @title API3DAO Revenue Incinerator

/** @notice immutable burn of excess USDC revenue by buying and burning API3 tokens per API3DAO whitepaper:
 *** swaps USDC sent to this contract for API3 tokens and ETH to LP in UniV2; each LP is then queued chronologically.
 *** LP position is redeemable to this contract after one year by any caller, which is swapped to API3 tokens via UniV2 and burned via the API3 token contract.
 *** ETH sent directly to this contract is auto-swapped to API3 tokens, which are then burned via the API3 token contract,
 *** providing an optional method for automated ETH revenue burning */

/** @dev be advised there is inherent slippage risk for large amounts,
 *** so it is recommended to call swapUSDCToAPI3AndLpWithETHPair() often, perhaps by bot;
 *** swapFractionUSDCToAPI3AndLpWithETHPair() may also be used to swap and LP less than the 
 *** total balance of USDC at a time and further avoid such risks. */

/// ⟁ ⟁ ⟁ /// INTERFACES /// ⟁ ⟁ ⟁ ///

interface IUniswapV2Router02 {
    function addLiquidityETH(
        address token,
        uint256 amountTokenDesired,
        uint256 amountTokenMin,
        uint256 amountETHMin,
        address to,
        uint256 deadline
    )
        external
        payable
        returns (
            uint256 amountToken,
            uint256 amountETH,
            uint256 liquidity
        );

    function removeLiquidityETH(
        address token,
        uint256 liquidity,
        uint256 amountTokenMin,
        uint256 amountETHMin,
        address to,
        uint256 deadline
    ) external returns (uint256 amountToken, uint256 amountETH);

    function swapExactETHForTokens(
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external payable returns (uint256[] memory amounts);

    function swapExactTokensForETH(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external returns (uint256[] memory amounts);
}

interface IAPI3 {
    function approve(address spender, uint256 amount) external returns (bool);

    function balanceOf(address account) external view returns (uint256);

    function burn(uint256 amount) external;

    function updateBurnerStatus(bool burnerStatus) external;
}

interface IERC20 {
    function approve(address spender, uint256 amount) external returns (bool);

    function balanceOf(address account) external view returns (uint256);
}

/// ⟁ ⟁ ⟁ /// CONTRACT /// ⟁ ⟁ ⟁ ///

contract API3DAORevenueIncinerator {
    struct Liquidity {
        uint256 withdrawTime;
        uint256 amount;
    }

    address internal constant API3_TOKEN_ADDR =
        0x0b38210ea11411557c13457D4dA7dC6ea731B88a;
    address internal constant LP_TOKEN_ADDR =
        0x4Dd26482738bE6C06C31467a19dcdA9AD781e8C4;
    address internal constant UNI_ROUTER_ADDR =
        0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
    address internal constant USDC_TOKEN_ADDR =
        0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
    address internal constant WETH_TOKEN_ADDR =
        0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

    uint256 internal constant YEAR_IN_SECONDS = 31557600;

    IUniswapV2Router02 public immutable router;
    IAPI3 public immutable iAPI3Token;

    uint256 public lpAddIndex;
    uint256 public lpRedeemIndex;

    mapping(uint256 => Liquidity) public liquidityAdds;

    /// ⟁ ⟁ ⟁ /// ERRORS /// ⟁ ⟁ ⟁ ///

    error NoAPI3Tokens();
    error NoRedeemableLPTokens();
    error NoUSDCTokens();
    error ZeroDivisor();

    /// ⟁⟁⟁ /// EVENTS /// ⟁⟁⟁ ///

    event API3Burned(uint256 amountBurned);
    event LiquidityProvided(uint256 liquidityAdded, uint256 indexed lpIndex, uint256 indexed timestamp);
    event LiquidityRemoved(uint256 liquidityRemoved, uint256 indexed lpIndex, uint256 indexed timestamp);

    /// ⟁ ⟁ ⟁ /// FUNCTIONS /// ⟁ ⟁ ⟁ ///

    /// @notice set interfaces, API3 token burner status for address(this) and approve Uniswap router for address(this) for API3, USDC, and API3/ETH LP tokens
    constructor() payable {
        address uniRouter = UNI_ROUTER_ADDR;
        router = IUniswapV2Router02(uniRouter);
        iAPI3Token = IAPI3(API3_TOKEN_ADDR);
        IAPI3(API3_TOKEN_ADDR).updateBurnerStatus(true);
        IAPI3(API3_TOKEN_ADDR).approve(uniRouter, type(uint256).max);
        IERC20(USDC_TOKEN_ADDR).approve(uniRouter, type(uint256).max);
        IERC20(LP_TOKEN_ADDR).approve(uniRouter, type(uint256).max);
    }

    /// @notice receives ETH sent to address(this), and if msg.sender != router, swaps for API3 tokens, and calls _burnAPI3()
    /// also useful for burning any leftover/dust API3 tokens held by or sent to this contract. Small amounts advisable to avoid MEV trouble.
    receive() external payable {
        if (msg.sender != UNI_ROUTER_ADDR) {
            router.swapExactETHForTokens{value: msg.value}(
                1,
                _getPathForETHtoAPI3(),
                address(this),
                block.timestamp
            );
            _burnAPI3();
        } else {}
    }

    /// @notice swaps all USDC in this contract for ETH, then swaps 1/2 of the ETH for API3 tokens, and LPs API3/ETH
    /*** @dev no authorisation restriction, callable by anyone. More favourable to call frequently in order to avoid
     *** substantial slippage / MEV risks */
    function swapUSDCToAPI3AndLpWithETHPair() external {
        uint256 _usdcBal = IERC20(USDC_TOKEN_ADDR).balanceOf(address(this));
        if (_usdcBal == 0) revert NoUSDCTokens();

        // swap all USDC for ETH
        router.swapExactTokensForETH(
            _usdcBal,
            1,
            _getPathForUSDCtoETH(),
            payable(address(this)),
            block.timestamp
        );

        // swap half of ETH for API3
        router.swapExactETHForTokens{
            value: address(this).balance / 2
        }(0, _getPathForETHtoAPI3(), address(this), block.timestamp);

        // LP
        _lpApi3Eth();
    }

    /// @notice swaps this address's USDC divided by _divisor for ETH, then swaps 1/2 of the ETH for API3 tokens, and LPs API3/ETH
    /** @dev no authorisation restriction, callable by anyone. Implemented to further avoid substantial slippage risks 
     ** by permitting < 100% of this contract's USDC be swapped at a time */
    /// @param _divisor divisor for division operation of this contract's USDC balance to then swap and LP, which must be > 0.
    /// for example, if _divisor = 2, 50% of USDC will be swapped, if _divisor = 4, 25%, etc.
    function swapFractionUSDCToAPI3AndLpWithETHPair(uint256 _divisor) external {
        if (_divisor == 0) revert ZeroDivisor();
        uint256 _swapAmount = IERC20(USDC_TOKEN_ADDR).balanceOf(address(this)) / _divisor;
        if (_swapAmount == 0) revert NoUSDCTokens();

        // swap USDC/_divisor for ETH
        router.swapExactTokensForETH(
            _swapAmount,
            1,
            _getPathForUSDCtoETH(),
            payable(address(this)),
            block.timestamp
        );

        // swap half of ETH for API3
        router.swapExactETHForTokens{
            value: address(this).balance / 2
        }(0, _getPathForETHtoAPI3(), address(this), block.timestamp);

        // LP
        _lpApi3Eth();
    }

    /** @dev checks earliest Liquidity struct to see if any LP tokens are redeemable,
     ** then redeems that amount of liquidity to this address (which is entirely converted and burned in API3 tokens via _burnAPI3()),
     ** then deletes that mapped struct in liquidityAdds[] and increments the lpRedeemIndex */
    /// @notice redeems the earliest available liquidity; redeemed API3 tokens are burned and redeemed ETH is converted to API3 tokens and burned
    function redeemLP() external {
        if (liquidityAdds[lpRedeemIndex].withdrawTime > block.timestamp)
            revert NoRedeemableLPTokens();
        uint256 _redeemableLpTokens = liquidityAdds[lpRedeemIndex].amount;

        // if lpRedeemIndex returns zero, increment the index; otherwise call _redeemLP and then increment
        if (_redeemableLpTokens == 0) {
            delete liquidityAdds[lpRedeemIndex];
            unchecked {
                ++lpRedeemIndex;
            }
        } else {
            _redeemLP(_redeemableLpTokens, lpRedeemIndex);
            unchecked {
                ++lpRedeemIndex;
            }
        }
    }

    /** @dev checks applicable Liquidity struct to see if any LP tokens are redeemable,
     ** then redeems that amount of liquidity to this address (which is entirely converted and burned in API3 tokens via _burnAPI3()),
     ** then deletes that mapped struct in liquidityAdds[]. Implemented in case of lpAddIndex--lpRedeemIndex mismatch */
    /// @notice redeems specifically indexed liquidity; redeemed API3 tokens are burned and redeemed ETH is converted to API3 tokens and burned
    /// @param _lpRedeemIndex: index of liquidity in liquidityAdds[] mapping to be redeemed
    function redeemSpecificLP(uint256 _lpRedeemIndex) external {
        if (liquidityAdds[_lpRedeemIndex].withdrawTime > block.timestamp)
            revert NoRedeemableLPTokens();
        uint256 _redeemableLpTokens = liquidityAdds[_lpRedeemIndex].amount;

        // if _lpRedeemIndex returns zero, delete mapping; otherwise call _redeemLP. Do not increment the global lpRedeemIndex
        if (_redeemableLpTokens == 0) {
            delete liquidityAdds[_lpRedeemIndex];
        } else {
            _redeemLP(_redeemableLpTokens, _lpRedeemIndex);
        }
    }

    /// @notice burns all API3 tokens held by this contract
    function _burnAPI3() internal {
        uint256 _api3Bal = iAPI3Token.balanceOf(address(this));
        if (_api3Bal == 0) revert NoAPI3Tokens();
        iAPI3Token.burn(_api3Bal);
        emit API3Burned(_api3Bal);
    }

    /// @notice LPs ETH and API3 tokens to UNIv2's API3/ETH pair
    /// @dev LP has 10% buffer. Liquidity locked for one year and queued chronologically.
    function _lpApi3Eth() internal {
        uint256 _api3Bal = iAPI3Token.balanceOf(address(this));
        if (_api3Bal == 0) revert NoAPI3Tokens();
        uint256 _ethBal = address(this).balance;
        (, , uint256 liquidity) = router.addLiquidityETH{value: _ethBal}(
            API3_TOKEN_ADDR,
            _api3Bal,
            (_api3Bal * 9) / 10, // 90% of the api3Bal
            (_ethBal * 9) / 10, // 90% of the ethBal
            payable(address(this)),
            block.timestamp
        );
        emit LiquidityProvided(liquidity, lpAddIndex, block.timestamp);
        unchecked {
            liquidityAdds[lpAddIndex] = Liquidity(
                block.timestamp + YEAR_IN_SECONDS, // will not overflow on human timelines
                liquidity
            );
            ++lpAddIndex;
        }
    }

    /// @notice redeems the LP for the corresponding lpRedeemIndex, swaps redeemed ETH for API3 tokens, and burns all API3 tokens
    /// @param _redeemableLpTokens amount of LP tokens available to redeem
    /// @param _lpRedeemIndex LP index being redeemed
    function _redeemLP(uint256 _redeemableLpTokens, uint256 _lpRedeemIndex)
        internal
    {
        router.removeLiquidityETH(
            API3_TOKEN_ADDR,
            _redeemableLpTokens,
            1,
            1,
            payable(address(this)),
            block.timestamp
        );
        delete liquidityAdds[_lpRedeemIndex];
        emit LiquidityRemoved(_redeemableLpTokens, _lpRedeemIndex, block.timestamp);

        // removed liquidity is 50/50 ETH and API3; swap all newly received ETH for API3, then burn all the API3 tokens
        router.swapExactETHForTokens{value: address(this).balance}(
            1,
            _getPathForETHtoAPI3(),
            address(this),
            block.timestamp
        );
        _burnAPI3();
    }

    /// @return path: the router path for ETH/API3 swap
    function _getPathForETHtoAPI3() internal pure returns (address[] memory) {
        address[] memory path = new address[](2);
        path[0] = WETH_TOKEN_ADDR;
        path[1] = API3_TOKEN_ADDR;
        return path;
    }

    /// @return path: the router path for USDC/ETH swap
    function _getPathForUSDCtoETH() internal pure returns (address[] memory) {
        address[] memory path = new address[](2);
        path[0] = USDC_TOKEN_ADDR;
        path[1] = WETH_TOKEN_ADDR;
        return path;
    }
}
3 Likes

Much appreciated, this indeed looks much more auditable. I’ll get back to you once that is done.

After reviewing the updated contract and favourable audit report we deployed and verified API3DAORevenueIncinerator at Ethereum mainnet address 0x04264df62040290622257F83ee110Ac43d36dE69

No revisions to the audited code were made save for additional commenting. We look forward to contributing more to the API3 DAO in the future. We can be contacted at oss_tech@proton.me.

OSS Technologies does not retain any control over the smart contracts or any assets mentioned in this message. The deployment instance is intended as an ownerless public good, as a suggested manner of programmatic USDC and ETH revenue treatment for the API3 DAO. We received no payment for this contribution, we make no representations as to its functionality, and make no commitments related to any future actions related hereto.

4 Likes

I built https://github.com/api3dao/revenue-incinerator/tree/75c624ee0e3115484f24a17d4d61fa7350990c81 with solc v0.8.18 and 1000 optimizer runs. The contract creation code of 0x04264df62040290622257F83ee110Ac43d36dE69 matches the deployedBytecode I got, except metadata is different, which is to be expected because I see on Etherscan that OSS has added some comments in the code. Note that anyone can do this check.

Apparently the repo I linked above is private and for a reason. Instead, you can refer to the audit report (funded by core technical team) and the code submitted to Etherscan by OSS. My understanding is that the audit findings are addressed with some additional improvements.

1 Like