• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

RigoBlock / v3-contracts / 13261352678

11 Feb 2025 10:59AM UTC coverage: 84.94% (+2.6%) from 82.368%
13261352678

Pull #622

github

web-flow
Merge f22d260e3 into 08bd3b51b
Pull Request #622: feat: automated nav

761 of 962 branches covered (79.11%)

Branch coverage included in aggregate %.

523 of 711 new or added lines in 28 files covered. (73.56%)

18 existing lines in 5 files now uncovered.

1698 of 1933 relevant lines covered (87.84%)

44.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.85
/contracts/protocol/extensions/EApps.sol
1
// SPDX-License-Identifier: Apache-2.0-or-later
2
/*
3

4
 Copyright 2025 Rigo Intl.
5

6
 Licensed under the Apache License, Version 2.0 (the "License");
7
 you may not use this file except in compliance with the License.
8
 You may obtain a copy of the License at
9

10
     http://www.apache.org/licenses/LICENSE-2.0
11

12
 Unless required by applicable law or agreed to in writing, software
13
 distributed under the License is distributed on an "AS IS" BASIS,
14
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 See the License for the specific language governing permissions and
16
 limitations under the License.
17

18
*/
19

20
pragma solidity 0.8.28;
21

22
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
23
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
24
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
25
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
26
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
27
import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol";
28
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
29
import {PositionInfo, PositionInfoLibrary} from "@uniswap/v4-periphery/src/libraries/PositionInfoLibrary.sol";
30
import {IERC721Enumerable as IERC721} from "forge-std/interfaces/IERC721.sol";
31
import {IEOracle} from "../../protocol/extensions/adapters/interfaces/IEOracle.sol";
32
import {ApplicationsLib} from "../../protocol/libraries/ApplicationsLib.sol";
33
import {StorageLib} from "../../protocol/libraries/StorageLib.sol";
34
import {IStaking} from "../../staking/interfaces/IStaking.sol";
35
import {IStorage} from "../../staking/interfaces/IStorage.sol";
36
import {INonfungiblePositionManager} from "../../utils/exchanges/uniswap/INonfungiblePositionManager/INonfungiblePositionManager.sol";
37
import {Applications, TokenIdsSlot} from "../types/Applications.sol";
38
import {AppTokenBalance, ExternalApp} from "../types/ExternalApp.sol";
39
import {Int256, TransientBalance} from "../types/TransientBalance.sol";
40
import {IEApps} from "./adapters/interfaces/IEApps.sol";
41

42
/// @notice A universal aggregator for external contracts positions.
43
/// @dev External positions are consolidating into a single view contract. As more apps are connected, can be split into multiple mixing.
44
/// @dev Future-proof as can route to dedicated extensions, should the size of the contract become too big.
45
contract EApps is IEApps {
46
    using ApplicationsLib for uint256;
47
    using StateLibrary for IPoolManager;
48
    using PositionInfoLibrary for PositionInfo;
49
    // TODO: check rename TransientBalance, or add one more method to store int24 price
50
    using TransientBalance for Int256;
51

52
    error UnknownApplication(uint256 appType);
53

54
    IStaking private immutable _grgStakingProxy;
55
    INonfungiblePositionManager private immutable _uniV3NPM;
56
    IPositionManager private immutable _uniV4Posm;
57

58
    // TODO: define immutable storage slot
59
    bytes32 private immutable _TRANSIENT_TICK_SLOT;
60
    int24 private constant outOfRangeFlag = -887273;
61

62
    /// @notice The different immutable addresses will result in different deployed addresses on different networks.
63
    constructor(address grgStakingProxy, address univ3Npm, address univ4Posm) {
64
        _grgStakingProxy = IStaking(grgStakingProxy);
14✔
65
        _uniV3NPM = INonfungiblePositionManager(univ3Npm);
14✔
66
        _uniV4Posm = IPositionManager(univ4Posm);
14✔
67

68
        _TRANSIENT_TICK_SLOT = keccak256(abi.encode("transient.tick.slot"));
14✔
69
    }
70

71
    struct Application {
72
        bool isActive;
73
    }
74

75
    /// @inheritdoc IEApps
76
    /// @notice Uses temporary storage to cache token prices, which can be used in MixinPoolValue.
77
    /// @notice Requires delegatecall.
78
    function getAppTokenBalances(uint256 packedApplications) external override returns (ExternalApp[] memory) {
79
        uint256 activeAppCount;
60✔
80
        Application[] memory apps = new Application[](uint256(Applications.COUNT));
60✔
81

82
        // Count how many applications are active
83
        for (uint256 i = 0; i < uint256(Applications.COUNT); i++) {
60✔
84
            if (packedApplications.isActiveApplication(uint256(Applications(i)))) {
180✔
85
                activeAppCount++;
21✔
86
                apps[i].isActive = true;
21✔
87
            // grg staking and univ3 liquidity are pre-existing applications that do not require an upgrade, so they are not
88
            // stored. However, future upgrades may change that and we use this fallback block until implemented.
89
            } else if (Applications(i) == Applications.GRG_STAKING || Applications(i) == Applications.UNIV3_LIQUIDITY) {
261✔
90
                activeAppCount++;
102✔
91
                apps[i].isActive = true;
102✔
92
            } else {
93
                continue;
57✔
94
            }
95
        }
96

97
        ExternalApp[] memory nestedBalances = new ExternalApp[](activeAppCount);
60✔
98
        uint256 activeAppIndex = 0;
60✔
99

100
        for (uint256 i = 0; i < uint256(Applications.COUNT); i++) {
60✔
101
            if (apps[i].isActive) {
180✔
102
                nestedBalances[activeAppIndex].balances = _handleApplication(Applications(i));
123✔
103
                nestedBalances[activeAppIndex].appType = uint256(Applications(i));
123✔
104
                activeAppIndex++;
123✔
105
            }
106
        }
107
        return nestedBalances;
60✔
108
    }
109

110
    /// @inheritdoc IEApps
111
    function getUniV4TokenIds() external view override returns (uint256[] memory tokenIds) {
NEW
112
        return StorageLib.uniV4TokenIdsSlot().tokenIds;
×
113
    }
114

115
    // TODO: uncomment applications after implementing univ4Posm in test pipeline
116
    /// @notice Directly retrieve balances from target application contract.
117
    /// @dev A failure to get response from one application will revert the entire call.
118
    /// @dev This is ok as we do not want to produce an inaccurate nav.
119
    function _handleApplication(Applications appType) private returns (AppTokenBalance[] memory balances) {
120
        if (appType == Applications.GRG_STAKING) {
123✔
121
            balances = _getGrgStakingProxyBalances();
60✔
122
        } else if (appType == Applications.UNIV3_LIQUIDITY) {
63✔
123
            balances = _getUniV3PmBalances();
60✔
124
        } else if (appType == Applications.UNIV4_LIQUIDITY) {
3!
125
            balances = _getUniV4PmBalances();
3✔
126
        } else {
NEW
127
            revert UnknownApplication(uint256(appType));
×
128
        }
129
    }
130

131
    /// @dev Will return an empty array in case no stake found but unclaimed rewards (which are earned in the undelegate epoch).
132
    /// @dev This is fine as the amount is very small and saves several storage reads.
133
    function _getGrgStakingProxyBalances() private view returns (AppTokenBalance[] memory balances) {
134
        uint256 stakingBalance = _grgStakingProxy.getTotalStake(address(this));
60✔
135

136
        // continue querying unclaimed rewards only with positive balance
137
        if (stakingBalance > 0) {
60✔
138
            balances = new AppTokenBalance[](1);
3✔
139
            balances[0].token = address(_grgStakingProxy.getGrgContract());
3✔
140
            bytes32 poolId = IStorage(address(_grgStakingProxy)).poolIdByRbPoolAccount(address(this));
3✔
141
            balances[0].amount += int256(stakingBalance + _grgStakingProxy.computeRewardBalanceOfDelegator(poolId, address(this)));
3✔
142
        }
143
    }
144

145
    /// @dev Using the oracle protects against manipulations of position tokens via slot0 (i.e. via flash loans)
146
    function _getUniV3PmBalances() private returns (AppTokenBalance[] memory balances) {
147
        uint256 numPositions = IERC721(address(_uniV3NPM)).balanceOf(address(this));
60✔
148
        // TODO: we could push active app here if positive balance
149

150
        // only get first 20 positions as no pool has more than that and we can save gas plus prevent DOS
151
        uint256 maxLength = numPositions < 20 ? numPositions : 20;
60!
152
        balances = new AppTokenBalance[](maxLength * 2);
60✔
153

154
        for (uint256 i = 0; i < maxLength; i++) {
60✔
155
            uint256 tokenId = IERC721(address(_uniV3NPM)).tokenOfOwnerByIndex(address(this), i);
17✔
156
            (,, address token0, address token1, , int24 tickLower, int24 tickUpper, uint128 liquidity, , , ,) =
17✔
157
                _uniV3NPM.positions(tokenId);
158

159
            // TODO: check if we should try and convert only with a valid price. Also check if we really resort to v4 lib, or if we added to lib/univ3
160
            // we resort to v4 LiquidityAmounts library, as PositionValue and FullMath in v3's LiquidityAmounts lib require solc <0.8
161
            // for simplicity, as uni v3 liquidity is remove-only, we exclude unclaimed fees, which incentivizes migrating liquidity to v4.
162
            (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
17✔
163
                _findCrossPrice(token0, token1),
164
                TickMath.getSqrtPriceAtTick(tickLower),
165
                TickMath.getSqrtPriceAtTick(tickUpper),
166
                liquidity
167
            );
168

169
            // TODO: technically, we could convert balances to ETH so we wouldn' need to store many tokens in memory. We should check what works best.
170
            balances[i * 2].token = token0;
17✔
171
            balances[i * 2].amount = int256(amount0);
17✔
172
            balances[i * 2 + 1].token = token1;
17✔
173
            balances[i * 2 + 1].amount = int256(amount1);
17✔
174
        }
175
    }
176

177
    /// @dev Assumes a hook does not influence liquidity. This is true as long as it cannot access after remove liquidity deltas.
178
    function _getUniV4PmBalances() private returns (AppTokenBalance[] memory balances) {
179
        // access stored position ids
180
        uint256[] memory tokenIds = StorageLib.uniV4TokenIdsSlot().tokenIds;
3✔
181
        uint256 length = tokenIds.length;
3✔
182
        balances = new AppTokenBalance[](length * 2);
3✔
183

184
        // a maximum of 255 positons can be created, so this loop will not break memory or block limits
185
        for (uint256 i = 0; i < length; i++) {
3✔
186
            (PoolKey memory poolKey, PositionInfo info) = _uniV4Posm.getPoolAndPositionInfo(tokenIds[i]);
3✔
187

188
            // we accept an evaluation error by excluding unclaimed fees, which can be inflated arbitrarily
189
            (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(
3✔
190
                _findCrossPrice(Currency.unwrap(poolKey.currency0), Currency.unwrap(poolKey.currency1)),
191
                TickMath.getSqrtPriceAtTick(info.tickLower()),
192
                TickMath.getSqrtPriceAtTick(info.tickUpper()),
193
                _uniV4Posm.getPositionLiquidity(tokenIds[i])
194
            );
195

196
            // TODO: it seems we are not returning any balances here?
197
            balances[i * 2].token = Currency.unwrap(poolKey.currency0);
3✔
198
            balances[i * 2].amount = int256(amount0);
3✔
199
            balances[i * 2 + 1].token = Currency.unwrap(poolKey.currency1);
3✔
200
            balances[i * 2 + 1].amount = int256(amount1);
3✔
201
        }
202
    }
203

204
    function _findCrossPrice(address token0, address token1) private returns (uint160) {
205
        int24 tick0 = int24(Int256.wrap(_TRANSIENT_TICK_SLOT).get(token0));
20✔
206
        int24 tick1 = int24(Int256.wrap(_TRANSIENT_TICK_SLOT).get(token1));
20✔
207
        uint16 cardinality;
20✔
208

209
        if (tick0 == 0) {
20!
210
            // TODO: we should probably get a TWAP, and/or make some assertions
211
            (tick0, cardinality) = IEOracle(address(this)).getTick(token0);
20✔
212

213
            if (cardinality == 0) {
20!
NEW
214
                tick0 = outOfRangeFlag;
×
215
            } else if (tick0 == 0) {
20!
216
                tick0 = 1;
20✔
217
            }
218
        }
219

220
        if (tick1 == 0) {
20!
221
            (tick1, cardinality) = IEOracle(address(this)).getTick(token1);
20✔
222

223
            if (cardinality == 0) {
20!
NEW
224
                tick1 = outOfRangeFlag;
×
225
            } else if (tick1 == 0) {
20✔
226
                tick1 = 1;
3✔
227
            }
228
        }
229

230
        Int256.wrap(_TRANSIENT_TICK_SLOT).store(token0, tick0);
20✔
231
        Int256.wrap(_TRANSIENT_TICK_SLOT).store(token1, tick1);
20✔
232

233
        if (tick0 == outOfRangeFlag || tick1 == outOfRangeFlag) {
20!
NEW
234
            return 0;
×
235
        }
236

237
        return TickMath.getSqrtPriceAtTick(tick0 - tick1);
20✔
238
    }
239
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc