• 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

96.12
/contracts/protocol/core/actions/MixinActions.sol
1
// SPDX-License-Identifier: Apache 2.0
2
pragma solidity >=0.8.0 <0.9.0;
3

4
import {MixinStorage} from "../immutable/MixinStorage.sol";
5
import {IEOracle} from "../../extensions/adapters/interfaces/IEOracle.sol";
6
import {IERC20} from "../../interfaces/IERC20.sol";
7
import {IKyc} from "../../interfaces/IKyc.sol";
8
import {IRigoblockV3PoolActions} from "../../interfaces/pool/IRigoblockV3PoolActions.sol";
9
import {ReentrancyGuardTransient} from "../../libraries/ReentrancyGuardTransient.sol";
10
import {Currency, SafeTransferLib} from "../../libraries/SafeTransferLib.sol";
11
import {NavComponents} from "../../types/NavComponents.sol";
12

13
abstract contract MixinActions is MixinStorage, ReentrancyGuardTransient {
14
    using SafeTransferLib for address;
15

16
    error BaseTokenBalance();
17
    error PoolAmountSmallerThanMinumum(uint16 minimumOrderDivisor);
18
    error PoolBurnNotEnough();
19
    error PoolBurnNullAmount();
20
    error PoolBurnOutputAmount();
21
    error PoolCallerNotWhitelisted();
22
    error PoolMinimumPeriodNotEnough();
23
    error PoolMintAmountIn();
24
    error PoolMintOutputAmount();
25
    error PoolSupplyIsNullOrDust();
26
    error PoolTokenNotActive();
27

28
    /*
29
     * EXTERNAL METHODS
30
     */
31
    /// @inheritdoc IRigoblockV3PoolActions
32
    function mint(
33
        address recipient,
34
        uint256 amountIn,
35
        uint256 amountOutMin
36
    ) public payable override nonReentrant returns (uint256 recipientAmount) {
67✔
37
        NavComponents memory components = _updateNav();
66✔
38
        address kycProvider = poolParams().kycProvider;
66✔
39

40
        // require whitelisted user if kyc is enforced
41
        if (!kycProvider.isAddressZero()) {
66✔
42
            require(IKyc(kycProvider).isWhitelistedUser(recipient), PoolCallerNotWhitelisted());
3✔
43
        }
44

45
        _assertBiggerThanMinimum(amountIn);
65✔
46

47
        if (components.baseToken.isAddressZero()) {
62✔
48
            require(msg.value == amountIn, PoolMintAmountIn());
39✔
49
        } else {
50
            components.baseToken.safeTransferFrom(msg.sender, address(this), amountIn);
23✔
51
        }
52

53
        bool isOnlyHolder = components.totalSupply == accounts().userAccounts[recipient].userBalance;
59✔
54

55
        if (!isOnlyHolder) {
59✔
56
            // apply markup
57
            amountIn -= (amountIn * _getSpread()) / _SPREAD_BASE;
6✔
58
        }
59

60
        uint256 mintedAmount = (amountIn * 10 ** components.decimals) / components.unitaryValue;
59✔
61
        require(mintedAmount > amountOutMin, PoolMintOutputAmount());
59✔
62
        poolTokens().totalSupply += mintedAmount;
58✔
63

64
        // allocate pool token transfers and log events.
65
        recipientAmount = _allocateMintTokens(recipient, mintedAmount);
58✔
66
    }
67

68
    /// @inheritdoc IRigoblockV3PoolActions
69
    function burn(uint256 amountIn, uint256 amountOutMin) external override nonReentrant returns (uint256 netRevenue) {
20!
70
        netRevenue = _burn(amountIn, amountOutMin, _BASE_TOKEN_FLAG);
20✔
71
    }
72

73
    // TODO: test for potential abuse. Technically, if the token can be manipulated, a burn in base token can do just as much
74
    // harm as a burn in any token. Considering burn must happen after a certain period, a pool opeartor has time to sell illiquid tokens.
75
    // technically, this could be used for exchanging big quantities of tokens at market rate. Which is not a big deal. prob should
76
    // allow only if user does not have enough base tokens
77
    /// @inheritdoc IRigoblockV3PoolActions
78
    function burnForToken(
79
        uint256 amountIn,
80
        uint256 amountOutMin,
81
        address tokenOut
82
    ) external override nonReentrant returns (uint256 netRevenue) {
16!
83
        // early revert if token does not have price feed, 0 is sentinel for token not being active. Removed token will revert later.
84
        // TODO: we also use type(uint256).max as flag for removed token
85
        require(activeTokensSet().positions[tokenOut] != 0, PoolTokenNotActive());
16✔
86
        netRevenue = _burn(amountIn, amountOutMin, tokenOut);
15✔
87
    }
88

89
    /// @inheritdoc IRigoblockV3PoolActions
90
    function setUnitaryValue() external override nonReentrant {
10!
91
        NavComponents memory components = _updateNav();
10✔
92

93
        // unitary value is updated only with non-dust supply
94
        require(components.totalSupply >= 1e2, PoolSupplyIsNullOrDust());
10✔
95
    }
96

97
    /*
98
     * PUBLIC METHODS
99
     */
100
    function decimals() public view virtual override returns (uint8);
101

102
    /*
103
     * INTERNAL METHODS
104
     */
105
    function _updateNav() internal virtual returns (NavComponents memory);
106

107
    function _getFeeCollector() internal view virtual returns (address);
108

109
    function _getMinPeriod() internal view virtual returns (uint48);
110

111
    /// @dev Returns the spread, or _MAX_SPREAD if not set
112
    function _getSpread() internal view virtual returns (uint16);
113

114
    /*
115
     * PRIVATE METHODS
116
     */
117
    /// @notice Allocates tokens to recipient. Fee tokens are locked too.
118
    /// @dev Each new mint on same recipient sets new activation on all owned tokens.
119
    /// @param recipient Address of the recipient.
120
    /// @param mintedAmount Value of issued tokens.
121
    /// @return Amount of tokens minted to the recipient.
122
    function _allocateMintTokens(address recipient, uint256 mintedAmount) private returns (uint256) {
123
        uint48 activation;
58✔
124

125
        // it is safe to use unckecked as max min period is 30 days
126
        unchecked {
58✔
127
            activation = uint48(block.timestamp) + _getMinPeriod();
58✔
128
        }
129

130
        uint16 transactionFee = poolParams().transactionFee;
58✔
131

132
        if (transactionFee != 0) {
58✔
133
            address feeCollector = _getFeeCollector();
5✔
134

135
            if (feeCollector != recipient) {
5✔
136
                uint256 feePool = (mintedAmount * transactionFee) / _FEE_BASE;
3✔
137
                mintedAmount -= feePool;
3✔
138

139
                // fee tokens are locked as well
140
                accounts().userAccounts[feeCollector].userBalance += uint208(feePool);
3✔
141
                accounts().userAccounts[feeCollector].activation = activation;
3✔
142
                emit Transfer(_ZERO_ADDRESS, feeCollector, feePool);
3✔
143
            }
144
        }
145

146
        accounts().userAccounts[recipient].userBalance += uint208(mintedAmount);
58✔
147
        accounts().userAccounts[recipient].activation = activation;
58✔
148
        emit Transfer(_ZERO_ADDRESS, recipient, mintedAmount);
58✔
149
        return mintedAmount;
58✔
150
    }
151

152
    function _burn(uint256 amountIn, uint256 amountOutMin, address tokenOut) private returns (uint256 netRevenue) {
153
        require(amountIn > 0, PoolBurnNullAmount());
35✔
154
        UserAccount memory userAccount = accounts().userAccounts[msg.sender];
32✔
155
        require(userAccount.userBalance >= amountIn, PoolBurnNotEnough());
32✔
156
        require(block.timestamp >= userAccount.activation, PoolMinimumPeriodNotEnough());
31✔
157

158
        // update stored pool value
159
        NavComponents memory components = _updateNav();
27✔
160

161
        /// @notice allocate pool token transfers and log events.
162
        uint256 burntAmount = _allocateBurnTokens(amountIn, userAccount.userBalance);
27✔
163
        bool isOnlyHolder = components.totalSupply == userAccount.userBalance;
27✔
164
        poolTokens().totalSupply -= burntAmount;
27✔
165

166
        if (!isOnlyHolder) {
27✔
167
            // apply markup
168
            burntAmount -= (burntAmount * _getSpread()) / _SPREAD_BASE;
2✔
169
        }
170

171
        // TODO: verify cases of possible underflow for small nav value
172
        netRevenue = (burntAmount * components.unitaryValue) / 10 ** decimals();
27✔
173

174
        address baseToken = pool().baseToken;
27✔
175

176
        // TODO: test how this could be exploited.
177
        if (tokenOut == _BASE_TOKEN_FLAG) {
27✔
178
            tokenOut = baseToken;
16✔
179
        } else if (tokenOut != baseToken) {
11!
180
            // TODO: verify if we really want to allow this only if base token balance not enough
181
            // only allow arbitrary token redemption as a fallback in case the pool does not hold enough base currency
182
            uint256 baseTokenBalance = baseToken.isAddressZero() ? address(this).balance : IERC20(baseToken).balanceOf(address(this));
11✔
183
            require(netRevenue > baseTokenBalance, BaseTokenBalance());
11✔
184
            try IEOracle(address(this)).convertTokenAmount(baseToken, netRevenue, tokenOut) returns (uint256 value) {
9✔
185
                netRevenue = value;
9✔
186
            } catch Error(string memory reason) {
UNCOV
187
                revert(reason);
×
188
            }
189
        }
190

191
        require(netRevenue >= amountOutMin, PoolBurnOutputAmount());
25✔
192

193
        if (tokenOut.isAddressZero()) {
23✔
194
            msg.sender.safeTransferNative(netRevenue);
13✔
195
        } else {
196
            tokenOut.safeTransfer(msg.sender, netRevenue);
10✔
197
        }
198
    }
199

200
    /// @notice Destroys tokens of holder.
201
    /// @dev Fee is paid in pool tokens, fee amount is not burnt.
202
    /// @param amountIn Value of tokens to be burnt.
203
    /// @param holderBalance The balance of the caller.
204
    /// @return Number of user burnt tokens.
205
    function _allocateBurnTokens(uint256 amountIn, uint256 holderBalance) private returns (uint256) {
206
        if (amountIn < holderBalance) {
27✔
207
            accounts().userAccounts[msg.sender].userBalance -= uint208(amountIn);
17✔
208
        } else {
209
            delete accounts().userAccounts[msg.sender];
10✔
210
        }
211

212
        // TODO: define from constants
213
        if (poolParams().transactionFee != uint256(0)) {
27✔
214
            address feeCollector = _getFeeCollector();
2✔
215

216
            if (msg.sender != feeCollector) {
2✔
217
                uint256 feePool = (amountIn * poolParams().transactionFee) / _FEE_BASE;
1✔
218
                amountIn -= feePool;
1✔
219

220
                // allocate fee tokens to fee collector
221
                accounts().userAccounts[feeCollector].userBalance += uint208(feePool);
1✔
222
                accounts().userAccounts[feeCollector].activation = uint48(block.timestamp + 1);
1✔
223
                emit Transfer(msg.sender, feeCollector, feePool);
1✔
224
            }
225
        }
226

227
        // TODO: verify as this is inconsistent with mint, i.e. fee comes from user here, from null address in mint
228
        emit Transfer(msg.sender, _ZERO_ADDRESS, amountIn);
27✔
229
        return amountIn;
27✔
230
    }
231

232
    function _assertBiggerThanMinimum(uint256 amount) private view {
233
        require(
65✔
234
            amount >= 10 ** decimals() / _MINIMUM_ORDER_DIVISOR,
235
            PoolAmountSmallerThanMinumum(_MINIMUM_ORDER_DIVISOR)
236
        );
237
    }
238
}
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