• 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.71
/contracts/protocol/core/state/MixinPoolValue.sol
1
// SPDX-License-Identifier: Apache-2.0-or-later
2
/*
3

4
 Copyright 2024 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 {MixinOwnerActions} from "../actions/MixinOwnerActions.sol";
23
import {IEApps} from "../../extensions/adapters/interfaces/IEApps.sol";
24
import {IEOracle} from "../../extensions/adapters/interfaces/IEOracle.sol";
25
import {IERC20} from "../../interfaces/IERC20.sol";
26
import {AddressSet, EnumerableSet} from "../../libraries/EnumerableSet.sol";
27
import {ApplicationsLib, ApplicationsSlot} from "../../libraries/ApplicationsLib.sol";
28
import {ExternalApp} from "../../types/ExternalApp.sol";
29
import {NavComponents} from "../../types/NavComponents.sol";
30
import {Int256, TransientBalance} from "../../types/TransientBalance.sol";
31

32
// TODO: check make catastrophic failure resistant, i.e. must always be possible to liquidate pool + must always
33
//  use base token balances. If cannot guarantee base token balances can be retrieved, pointless and can be implemented in extension.
34
//  General idea is if the component is simple and not requires revisit of logic, implement as library, otherwise
35
//  implement as extension.
36
abstract contract MixinPoolValue is MixinOwnerActions {
37
    using ApplicationsLib for ApplicationsSlot;
38
    using EnumerableSet for AddressSet;
39
    using TransientBalance for Int256;
40

41
    error BaseTokenBalanceError();
42
    error BaseTokenPriceFeedError();
43

44
    // TODO: assert not possible to inflate total supply to manipulate pool price.
45
    /// @notice Uses transient storage to keep track of unique token balances.
46
    /// @dev With null total supply a pool will return the last stored value.
47
    function _updateNav() internal override returns (NavComponents memory components) {
48
        components.unitaryValue = poolTokens().unitaryValue;
103✔
49
        components.totalSupply = poolTokens().totalSupply;
103✔
50
        components.baseToken = pool().baseToken;
103✔
51
        components.decimals = pool().decimals;
103✔
52

53
        // first mint skips nav calculation
54
        if (components.unitaryValue == 0) {
103✔
55
            components.unitaryValue = 10 ** components.decimals;
46✔
56
        } else if (components.totalSupply == 0) {
57✔
57
            return components;
3✔
58
        } else {
59
            uint256 totalPoolValue = _computeTotalPoolValue(components.baseToken);
54✔
60

61
            // TODO: verify under what scenario totalPoolValue would be null here
62
            if (totalPoolValue > 0) {
54!
63
                // TODO: verify why we missed decimals rescaling
64
                // unitary value needs to be scaled by pool decimals (same as base token decimals)
65
                components.unitaryValue = totalPoolValue * 10 ** components.decimals / components.totalSupply;
54✔
66
            } else {
NEW
67
                return components;
×
68
            }
69
        }
70

71
        // unitary value cannot be null
72
        assert(components.unitaryValue > 0);
100✔
73

74
        // update storage only if different
75
        if (components.unitaryValue != poolTokens().unitaryValue) {
100✔
76
            poolTokens().unitaryValue = components.unitaryValue;
73✔
77
            emit NewNav(msg.sender, address(this), components.unitaryValue);
73✔
78
        }
79
    }
80

81
    /// @notice Updates the stored value with an updated one.
82
    /// @param baseToken The address of the base token.
83
    /// @return poolValue The total value of the pool in base token units.
84
    /// @dev Assumes the stored list contain unique elements.
85
    /// @dev A write method to be used in mint and burn operations.
86
    /// @dev Uses transient storage to keep track of unique token balances.
87
    function _computeTotalPoolValue(address baseToken) private returns (uint256 poolValue) {
88
        AddressSet storage values = activeTokensSet();
54✔
89
        int256 storedBalance;
54✔
90

91
        ApplicationsSlot storage appsBitmap = activeApplications();
54✔
92
        uint256 packedApps = appsBitmap.packedApplications;
54✔
93

94
        // try and get positions balances. Will revert if not successul and prevent incorrect nav calculation.
95
        try IEApps(address(this)).getAppTokenBalances(_getActiveApplications()) returns (ExternalApp[] memory apps) {
54✔
96
            // position balances can be negative, positive, or null (handled explicitly later)
97
            for (uint256 i = 0; i < apps.length; i++) {
54✔
98
                // active positions tokens are a subset of active tokens
99
                for (uint256 j = 0; j < apps[i].balances.length; j++) {
110✔
100
                    // push application if not active but tokens are returned from it (as with GRG staking and univ3 liquidity)
101
                    if (!ApplicationsLib.isActiveApplication(packedApps, uint256(apps[i].appType))) {
39✔
102
                        activeApplications().storeApplication(apps[i].appType);
15✔
103
                    }
104

105
                    // Always add or update the balance from positions
106
                    if (apps[i].balances[j].amount != 0) {
39✔
107
                        storedBalance = Int256.wrap(_TRANSIENT_BALANCE_SLOT).get(apps[i].balances[j].token);
36✔
108

109
                        // verify token in active tokens set, add it otherwise (relevant for pool deployed before v4)
110
                        if (storedBalance == 0) {
36✔
111
                            // will add to set only if not already stored
112
                            values.addUnique(IEOracle(address(this)), apps[i].balances[j].token, baseToken);
33✔
113
                        }
114

115
                        storedBalance += int256(apps[i].balances[j].amount);
36✔
116
                        // store balance and make sure slot is not cleared to prevent trying to add token again
117
                        Int256.wrap(_TRANSIENT_BALANCE_SLOT).store(apps[i].balances[j].token, storedBalance != 0 ? storedBalance : int256(1));
36✔
118
                    }
119
                }
120
            }
121
        } catch Error(string memory reason) {
122
            // we prevent returning pool value when any of the tracked applications fails
NEW
123
            revert(reason);
×
124
        }
125

126
        // active tokens include any potentially not stored app token , like when a pool upgrades from v3 to v4
127
        address[] memory activeTokens = activeTokensSet().addresses;
54✔
128
        uint256 length = activeTokens.length;
54✔
129
        address targetToken;
54✔
130
        int256 poolValueInBaseToken;
54✔
131

132
        // assert we can convert token values to base token. If there are no active tokens, all balance is in the base token
133
        if (length != 0) {
54✔
134
            // TODO: we can simply require, as eOracle is known in advance
135
            // make sure we can convert token values in base token
136
            try IEOracle(address(this)).hasPriceFeed(baseToken) returns (bool hasFeed) {
26✔
137
                require (hasFeed, BaseTokenPriceFeedError());
26!
138
            } catch Error(string memory reason) {
NEW
139
                revert(reason);
×
140
            }
141
        }
142

143
        // base token is not stored in activeTokens slot, so we add it as an additional element at the end of the loop
144
        for (uint256 i = 0; i <= length; i++) {
54✔
145
            targetToken = i == length ? baseToken : activeTokens[i];
92✔
146
            storedBalance = Int256.wrap(_TRANSIENT_BALANCE_SLOT).get(targetToken);
92✔
147

148
            // clear temporary storage if used
149
            if (storedBalance != 0) {
92✔
150
                Int256.wrap(_TRANSIENT_BALANCE_SLOT).store(targetToken, 0);
33✔
151
            }
152

153
            // the active tokens list contains unique addresses
154
            if (targetToken == _ZERO_ADDRESS) {
92✔
155
                storedBalance += int256(address(this).balance - msg.value);
42✔
156
            } else {
157
                try IERC20(targetToken).balanceOf(address(this)) returns (uint256 _balance) {
50✔
158
                    storedBalance += int256(_balance);
50✔
159
                } catch {
160
                    // do not stop aum calculation in case of chain's base currency or rogue token
NEW
161
                    continue;
×
162
                }
163
            }
164

165
            // convert wrapped native to native to potentially skip one or more conversions
166
            if (targetToken == wrappedNative) {
92!
NEW
167
                targetToken = _ZERO_ADDRESS;
×
168
            }
169

170
            // base token is always appended at the end of the loop
171
            if (baseToken == wrappedNative) {
92!
NEW
172
                baseToken = _ZERO_ADDRESS;
×
173
            }
174

175
            if (storedBalance < 0) {
92!
NEW
176
                poolValueInBaseToken -= int256(_getBaseTokenValue(targetToken, uint256(-storedBalance), baseToken));
×
177
            } else {
178
                poolValueInBaseToken += int256(_getBaseTokenValue(targetToken, uint256(storedBalance), baseToken));
92✔
179
            }
180
        }
181

182
        // TODO: verify why we return 1 with mint in base token
183
        // we never return 0, so updating stored value won't clear storage, i.e. an empty slot means a non-minted pool
184
        return (uint256(poolValueInBaseToken) > 0 ? uint256(poolValueInBaseToken) : 1);
54!
185
    }
186

187
    function _getBaseTokenValue(address token, uint256 amount, address baseToken) private view returns (uint256) {
188
        if (token == baseToken || amount == 0) {
92✔
189
            return amount;
56✔
190
        }
191

192
        // perform a staticcall to oracle extension
193
        try IEOracle(address(this)).convertTokenAmount(token, amount, baseToken) returns (uint256 value) {
36✔
194
            return value;
36✔
195
        } catch Error(string memory reason) {
NEW
196
            revert(reason);
×
197
        }
198
    }
199

200
    /// virtual methods
201
    function _getActiveApplications() internal view virtual returns (uint256);
202
}
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