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

nayms / contracts-v3 / 12292198243

12 Dec 2024 08:03AM UTC coverage: 93.502% (+0.2%) from 93.319%
12292198243

Pull #156

github

web-flow
Merge e06dbaa83 into 1dd4d9708
Pull Request #156: Adjust withdrawn amount when same coin is used to pay out a dividends

289 of 366 branches covered (78.96%)

Branch coverage included in aggregate %.

9 of 9 new or added lines in 1 file covered. (100.0%)

3 existing lines in 1 file now uncovered.

1409 of 1450 relevant lines covered (97.17%)

1228.36 hits per line

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

92.76
/src/libs/LibTokenizedVault.sol
1
// SPDX-License-Identifier: MIT
2
pragma solidity 0.8.20;
3

4
import { AppStorage, LibAppStorage } from "../shared/AppStorage.sol";
5
import { LibAdmin } from "./LibAdmin.sol";
6
import { LibConstants as LC } from "./LibConstants.sol";
7
import { LibHelpers } from "./LibHelpers.sol";
8
import { LibObject } from "./LibObject.sol";
9
import { LibERC20 } from "./LibERC20.sol";
10
import { RebasingInterestNotInitialized, RebasingInterestInsufficient, RebasingSupplyDecreased } from "../shared/CustomErrors.sol";
11

12
import { InsufficientBalance } from "../shared/CustomErrors.sol";
13

14
library LibTokenizedVault {
15
    /**
16
     * @dev Emitted when a token balance gets updated.
17
     * @param ownerId Id of owner
18
     * @param tokenId ID of token
19
     * @param newAmountOwned new amount owned
20
     * @param functionName Function name
21
     * @param msgSender msg.sender
22
     */
23
    event InternalTokenBalanceUpdate(bytes32 indexed ownerId, bytes32 tokenId, uint256 newAmountOwned, string functionName, address indexed msgSender);
24

25
    /**
26
     * @dev Emitted when a token supply gets updated.
27
     * @param tokenId ID of token
28
     * @param newTokenSupply New token supply
29
     * @param functionName Function name
30
     * @param msgSender msg.sender
31
     */
32
    event InternalTokenSupplyUpdate(bytes32 indexed tokenId, uint256 newTokenSupply, string functionName, address indexed msgSender);
33

34
    /**
35
     * @dev Emitted when a dividend gets paid out.
36
     * @param guid dividend distribution ID
37
     * @param from distribution initiator
38
     * @param to distribution receiver
39
     * @param amount distributed amount
40
     */
41
    event DividendDistribution(bytes32 indexed guid, bytes32 from, bytes32 to, bytes32 dividendTokenId, uint256 amount);
42

43
    /**
44
     * @dev Emitted when a dividend gets paid out.
45
     * @param accountId ID of the account withdrawing the dividend
46
     * @param tokenId ID of the participation token that is paying out the dividends to holders
47
     * @param amountOwned owned amount of the participation tokens
48
     * @param dividendTokenId ID of the dividend denomination token
49
     * @param dividendAmountWithdrawn amount withdrawn
50
     */
51
    event DividendWithdrawn(bytes32 indexed accountId, bytes32 tokenId, uint256 amountOwned, bytes32 dividendTokenId, uint256 dividendAmountWithdrawn);
52

53
    function _internalBalanceOf(bytes32 _ownerId, bytes32 _tokenId) internal view returns (uint256) {
54
        AppStorage storage s = LibAppStorage.diamondStorage();
6,300✔
55
        return s.tokenBalances[_tokenId][_ownerId];
6,300✔
56
    }
57

58
    function _internalTokenSupply(bytes32 _objectId) internal view returns (uint256) {
59
        AppStorage storage s = LibAppStorage.diamondStorage();
13,852✔
60
        return s.tokenSupply[_objectId];
13,852✔
61
    }
62

63
    function _internalTransfer(bytes32 _from, bytes32 _to, bytes32 _tokenId, uint256 _amount) internal returns (bool success) {
64
        AppStorage storage s = LibAppStorage.diamondStorage();
5,276✔
65

66
        if (s.tokenBalances[_tokenId][_from] < _amount) revert InsufficientBalance(_tokenId, _from, s.tokenBalances[_tokenId][_from], _amount);
5,276✔
67
        require(s.tokenBalances[_tokenId][_from] - s.lockedBalances[_from][_tokenId] >= _amount, "_internalTransfer: insufficient balance available, funds locked");
5,275✔
68

69
        _withdrawAllDividends(_from, _tokenId);
5,260✔
70

71
        s.tokenBalances[_tokenId][_from] -= _amount;
5,260✔
72
        s.tokenBalances[_tokenId][_to] += _amount;
5,260✔
73

74
        _normalizeDividends(_from, _to, _tokenId, _amount, false);
5,260✔
75

76
        emit InternalTokenBalanceUpdate(_from, _tokenId, s.tokenBalances[_tokenId][_from], "_internalTransfer", msg.sender);
5,260✔
77
        emit InternalTokenBalanceUpdate(_to, _tokenId, s.tokenBalances[_tokenId][_to], "_internalTransfer", msg.sender);
5,260✔
78

79
        success = true;
5,260✔
80
    }
81

82
    function _internalMint(bytes32 _to, bytes32 _tokenId, uint256 _amount) internal {
83
        require(_to != "", "_internalMint: mint to zero address");
4,117!
84
        require(_amount > 0, "_internalMint: mint zero tokens");
4,117!
85

86
        AppStorage storage s = LibAppStorage.diamondStorage();
4,117✔
87

88
        _normalizeDividends(bytes32(0), _to, _tokenId, _amount, true);
4,117✔
89

90
        s.tokenSupply[_tokenId] += _amount;
4,117✔
91
        s.tokenBalances[_tokenId][_to] += _amount;
4,117✔
92

93
        emit InternalTokenSupplyUpdate(_tokenId, s.tokenSupply[_tokenId], "_internalMint", msg.sender);
4,117✔
94
        emit InternalTokenBalanceUpdate(_to, _tokenId, s.tokenBalances[_tokenId][_to], "_internalMint", msg.sender);
4,117✔
95
    }
96

97
    function _normalizeDividends(bytes32 _from, bytes32 _to, bytes32 _tokenId, uint256 _amount, bool _updateTotals) internal {
98
        AppStorage storage s = LibAppStorage.diamondStorage();
9,377✔
99
        uint256 supply = _internalTokenSupply(_tokenId);
9,377✔
100

101
        // This must be done BEFORE the supply increases!!!
102
        // This will calculate the hypothetical dividends that would correspond to this number of shares.
103
        // It must be added to the withdrawn dividend for every denomination for the user who receives the minted tokens
104
        bytes32[] memory dividendDenominations = s.dividendDenominations[_tokenId];
9,377✔
105

106
        for (uint256 i = 0; i < dividendDenominations.length; ++i) {
9,377✔
107
            bytes32 dividendDenominationId = dividendDenominations[i];
266✔
108
            uint256 totalDividend = s.totalDividends[_tokenId][dividendDenominationId];
266✔
109

110
            // Dividend deduction for newly issued shares
111
            uint256 dividendDeductionIssued = _getWithdrawableDividendAndDeductionMath(_amount, supply, totalDividend, 0);
266✔
112

113
            // Scale total dividends and withdrawn dividend for new owner
114
            s.withdrawnDividendPerOwner[_tokenId][dividendDenominationId][_to] += dividendDeductionIssued;
266✔
115

116
            // Scale total dividends for the previous owner, if applicable
117
            if (_from != bytes32(0)) {
266✔
118
                s.withdrawnDividendPerOwner[_tokenId][dividendDenominationId][_from] -= dividendDeductionIssued;
265✔
119
            }
120

121
            if (_updateTotals) {
1✔
122
                s.totalDividends[_tokenId][dividendDenominationId] += (s.totalDividends[_tokenId][dividendDenominationId] * _amount) / supply;
1✔
123
            }
124
        }
125
    }
126

127
    /// @dev Recalculate totalDividends for all denominations when tokens are burned
128
    function _normalizeDividendsBurn(bytes32 _tokenId, uint256 _amount) internal {
129
        AppStorage storage s = LibAppStorage.diamondStorage();
298✔
130
        uint256 supply = _internalTokenSupply(_tokenId);
298✔
131

132
        bytes32[] memory dividendDenominations = s.dividendDenominations[_tokenId];
298✔
133

134
        for (uint256 i; i < dividendDenominations.length; ++i) {
298✔
135
            bytes32 dividendDenominationId = dividendDenominations[i];
8✔
136

137
            // new total dividends = old total dividends * new total supply of p token / old total supply of p token
138
            s.totalDividends[_tokenId][dividendDenominationId] = (s.totalDividends[_tokenId][dividendDenominationId] * (supply - _amount)) / supply;
8✔
139
        }
140
    }
141

142
    function _internalBurn(bytes32 _from, bytes32 _tokenId, uint256 _amount) internal {
143
        AppStorage storage s = LibAppStorage.diamondStorage();
301✔
144

145
        require(s.tokenBalances[_tokenId][_from] >= _amount, "_internalBurn: insufficient balance");
301!
146
        require(s.tokenBalances[_tokenId][_from] - s.lockedBalances[_from][_tokenId] >= _amount, "_internalBurn: insufficient balance available, funds locked");
301✔
147

148
        _withdrawAllDividends(_from, _tokenId);
298✔
149
        _normalizeDividendsBurn(_tokenId, _amount);
298✔
150
        s.tokenSupply[_tokenId] -= _amount;
298✔
151
        s.tokenBalances[_tokenId][_from] -= _amount;
298✔
152

153
        emit InternalTokenSupplyUpdate(_tokenId, s.tokenSupply[_tokenId], "_internalBurn", msg.sender);
298✔
154
        emit InternalTokenBalanceUpdate(_from, _tokenId, s.tokenBalances[_tokenId][_from], "_internalBurn", msg.sender);
298✔
155
    }
156

157
    //   DIVIDEND PAYOUT LOGIC
158
    //
159
    // When a dividend is paid, you divide by the total supply and add it to the totalDividendPerToken
160
    // Dividends are held by the diamond contract at: LibHelpers._stringToBytes32(LibConstants.DIVIDEND_BANK_IDENTIFIER)
161
    // When dividends are paid, they are transferred OUT of that same diamond contract ID.
162
    //
163
    // To calculate withdrawableDividend = ownedTokens * totalDividendPerToken - totalWithdrawnDividendPerOwner
164
    //
165
    // When a dividend is collected you set the totalWithdrawnDividendPerOwner to the total amount the owner withdrew
166
    //
167
    // When you transfer, you pay out all dividends to previous owner first, then transfer ownership
168
    // !!!YOU ALSO TRANSFER totalWithdrawnDividendPerOwner for those shares!!!
169
    // totalWithdrawnDividendPerOwner(for new owner) += numberOfSharesTransferred * totalDividendPerToken
170
    // totalWithdrawnDividendPerOwner(for previous owner) -= numberOfSharesTransferred * totalDividendPerToken (can be optimized)
171
    //
172
    // When minting
173
    // Add the token balance to the new owner
174
    // totalWithdrawnDividendPerOwner(for new owner) += numberOfSharesMinted * totalDividendPerToken
175
    //
176
    // When doing the division these will be dust. Leave the dust in the diamond!!!
177
    function _withdrawDividend(bytes32 _ownerId, bytes32 _tokenId, bytes32 _dividendTokenId) internal {
178
        AppStorage storage s = LibAppStorage.diamondStorage();
528✔
179
        bytes32 dividendBankId = LibHelpers._stringToBytes32(LC.DIVIDEND_BANK_IDENTIFIER);
528✔
180

181
        uint256 amountOwned = s.tokenBalances[_tokenId][_ownerId];
528✔
182
        uint256 supply = _internalTokenSupply(_tokenId);
528✔
183
        uint256 totalDividend = s.totalDividends[_tokenId][_dividendTokenId];
528✔
184
        uint256 withdrawnSoFar = s.withdrawnDividendPerOwner[_tokenId][_dividendTokenId][_ownerId];
528✔
185

186
        uint256 withdrawableDividend = _getWithdrawableDividendAndDeductionMath(amountOwned, supply, totalDividend, withdrawnSoFar);
528✔
187
        if (withdrawableDividend > 0) {
528✔
188
            // Bump the withdrawn dividends for the owner
189
            /// Special Case: (_tokenId == _dividendTokenId), i.e distributing accrued interest for rebasing coins like USDM
190
            /// withdrawnDividendPerOwner should be adjusted before tha update, so that the user cannot claim additional dividend based on the amount he just received as dividend
191
            /// dividend is calculated based on a ratio between users balance and the total, but in this case claiming the dividend his balance increases and
192
            /// thus his share of the total increases as well, which entitles him to claim more of the dividend, potentially draining out the entirety of it if repeated infinitely
193
            if (_tokenId == _dividendTokenId) {
297✔
194
                uint256 withdrawableDividendAdjusted = _getWithdrawableDividendAndDeductionMath(amountOwned + withdrawableDividend, supply, totalDividend, withdrawnSoFar);
4✔
195
                s.withdrawnDividendPerOwner[_tokenId][_dividendTokenId][_ownerId] += withdrawableDividendAdjusted;
4✔
196
            } else {
197
                s.withdrawnDividendPerOwner[_tokenId][_dividendTokenId][_ownerId] += withdrawableDividend;
293✔
198
            }
199

200
            // Move the dividend
201
            s.tokenBalances[_dividendTokenId][dividendBankId] -= withdrawableDividend;
297✔
202
            s.tokenBalances[_dividendTokenId][_ownerId] += withdrawableDividend;
297✔
203

204
            emit InternalTokenBalanceUpdate(dividendBankId, _dividendTokenId, s.tokenBalances[_dividendTokenId][dividendBankId], "_withdrawDividend", msg.sender);
297✔
205
            emit InternalTokenBalanceUpdate(_ownerId, _dividendTokenId, s.tokenBalances[_dividendTokenId][_ownerId], "_withdrawDividend", msg.sender);
297✔
206
            emit DividendWithdrawn(_ownerId, _tokenId, amountOwned, _dividendTokenId, withdrawableDividend);
297✔
207
        }
208
    }
209

210
    function _getWithdrawableDividend(bytes32 _ownerId, bytes32 _tokenId, bytes32 _dividendTokenId) internal view returns (uint256 withdrawableDividend_) {
211
        AppStorage storage s = LibAppStorage.diamondStorage();
1,044✔
212

213
        uint256 amount = s.tokenBalances[_tokenId][_ownerId];
1,044✔
214
        uint256 supply = _internalTokenSupply(_tokenId);
1,044✔
215
        uint256 totalDividend = s.totalDividends[_tokenId][_dividendTokenId];
1,044✔
216
        uint256 withdrawnSoFar = s.withdrawnDividendPerOwner[_tokenId][_dividendTokenId][_ownerId];
1,044✔
217

218
        withdrawableDividend_ = _getWithdrawableDividendAndDeductionMath(amount, supply, totalDividend, withdrawnSoFar);
1,044✔
219
    }
220

221
    function _withdrawAllDividends(bytes32 _ownerId, bytes32 _tokenId) internal {
222
        AppStorage storage s = LibAppStorage.diamondStorage();
5,560✔
223
        bytes32[] memory dividendDenominations = s.dividendDenominations[_tokenId];
5,560✔
224

225
        for (uint256 i = 0; i < dividendDenominations.length; ++i) {
5,560✔
226
            _withdrawDividend(_ownerId, _tokenId, dividendDenominations[i]);
274✔
227
        }
228
    }
229

230
    function _payDividend(bytes32 _guid, bytes32 _from, bytes32 _to, bytes32 _dividendTokenId, uint256 _amount) internal {
231
        require(_amount > 0, "dividend amount must be > 0");
773!
232
        require(LibAdmin._isSupportedExternalToken(_dividendTokenId), "must be supported dividend token");
773!
233
        require(!LibObject._isObject(_guid), "nonunique dividend distribution identifier");
773✔
234

235
        AppStorage storage s = LibAppStorage.diamondStorage();
772✔
236
        bytes32 dividendBankId = LibHelpers._stringToBytes32(LC.DIVIDEND_BANK_IDENTIFIER);
772✔
237

238
        // If no tokens are issued, then deposit directly.
239
        // note: This functionality is for the business case where we want to distribute dividends directly to entities.
240
        // How this functionality is implemented may be changed in the future.
241
        if (_internalTokenSupply(_to) == 0) {
772✔
242
            _internalTransfer(_from, _to, _dividendTokenId, _amount);
1✔
243
        }
244
        // Otherwise pay as dividend
245
        else {
246
            // issue dividend. if you are owed dividends on the _dividendTokenId, they will be collected
247
            // Check for possible infinite loop, but probably not
248
            _internalTransfer(_from, dividendBankId, _dividendTokenId, _amount);
771✔
249
            uint256 tokenSupply = LibTokenizedVault._internalTokenSupply(_dividendTokenId);
771✔
250
            uint256 adjustedDividendAmount = _amount;
771✔
251
            if (_to == _dividendTokenId) {
771✔
252
                // withdrawn dividend was adjusted for the previous holder in this case,
253
                // therfore dividend amount should be increased in order to give existing token holders the correct amount
254
                adjustedDividendAmount = (_amount * (tokenSupply)) / (tokenSupply - _amount);
2✔
255
            }
256

257
            s.totalDividends[_to][_dividendTokenId] += adjustedDividendAmount;
771✔
258

259
            // keep track of the dividend denominations
260
            // if dividend has not yet been issued in this token, add it to the list and update mappings
261
            if (s.dividendDenominationIndex[_to][_dividendTokenId] == 0 && s.dividendDenominationAtIndex[_to][0] != _dividendTokenId) {
771✔
262
                // We must limit the number of different tokens dividends are paid in
263
                if (s.dividendDenominations[_to].length >= LibAdmin._getMaxDividendDenominations()) {
506!
UNCOV
264
                    revert("exceeds max div denominations");
×
265
                }
266

267
                s.dividendDenominationIndex[_to][_dividendTokenId] = uint8(s.dividendDenominations[_to].length);
506✔
268
                s.dividendDenominationAtIndex[_to][uint8(s.dividendDenominations[_to].length)] = _dividendTokenId;
506✔
269
                s.dividendDenominations[_to].push(_dividendTokenId);
506✔
270
            }
271
        }
272

273
        // prevent guid reuse/collision
274
        LibObject._createObject(_guid, LC.OBJECT_TYPE_DIVIDEND);
772✔
275

276
        // Events are emitted from the _internalTransfer()
277
        emit DividendDistribution(_guid, _from, _to, _dividendTokenId, _amount);
772✔
278
    }
279

280
    function _getWithdrawableDividendAndDeductionMath(
281
        uint256 _amount,
282
        uint256 _supply,
283
        uint256 _totalDividend,
284
        uint256 _withdrawnSoFar
285
    ) internal pure returns (uint256 _withdrawableDividend) {
286
        // The holder dividend is: holderDividend = (totalDividend/tokenSupply) * _amount. The remainder (dust) is lost.
287
        uint256 totalDividendTimesAmount = _totalDividend * _amount;
3,435✔
288
        uint256 holderDividend = _supply == 0 ? 0 : (totalDividendTimesAmount / _supply);
3,435✔
289

290
        _withdrawableDividend = (_withdrawnSoFar >= holderDividend) ? 0 : holderDividend - _withdrawnSoFar;
3,435✔
291
    }
292

293
    function _getLockedBalance(bytes32 _accountId, bytes32 _tokenId) internal view returns (uint256 amount) {
294
        AppStorage storage s = LibAppStorage.diamondStorage();
590✔
295
        return s.lockedBalances[_accountId][_tokenId];
590✔
296
    }
297

298
    function _getAvailableBalance(bytes32 _accountId, bytes32 _tokenId) internal view returns (uint256 amount) {
299
        AppStorage storage s = LibAppStorage.diamondStorage();
1✔
300
        uint256 lockedBalance = s.lockedBalances[_accountId][_tokenId];
1✔
301
        uint256 internalBalance = s.tokenBalances[_tokenId][_accountId];
1✔
302
        return internalBalance - lockedBalance;
1✔
303
    }
304

305
    function _totalDividends(bytes32 _tokenId, bytes32 _dividendDenominationId) internal view returns (uint256) {
UNCOV
306
        AppStorage storage s = LibAppStorage.diamondStorage();
×
307

308
        return s.totalDividends[_tokenId][_dividendDenominationId];
×
309
    }
310

311
    function _accruedInterest(bytes32 _tokenId) internal view returns (uint256) {
312
        AppStorage storage s = LibAppStorage.diamondStorage();
6✔
313

314
        address tokenAddress = LibHelpers._getAddressFromId(_tokenId);
6✔
315

316
        // uint256 depositTotal = s.depositTotal[_tokenId];
317
        uint256 depositTotal = s.tokenSupply[_tokenId];
6✔
318
        uint256 total = LibERC20.balanceOf(tokenAddress, address(this));
6✔
319

320
        // If the Nayms balance of the rebasing token has decreased and is lower than the deposit total, revert
321
        if (total < depositTotal) {
6!
UNCOV
322
            revert RebasingSupplyDecreased(_tokenId, depositTotal, total);
×
323
        }
324
        return total - depositTotal;
6✔
325
    }
326

327
    function _claimRebasingInterest(bytes32 _tokenId, uint256 _amount) internal {
328
        AppStorage storage s = LibAppStorage.diamondStorage();
4✔
329

330
        if (s.tokenSupply[_tokenId] == 0) {
4✔
331
            revert RebasingInterestNotInitialized(_tokenId);
1✔
332
        }
333

334
        uint256 accruedAmount = _accruedInterest(_tokenId);
3✔
335
        if (_amount > accruedAmount) {
3✔
336
            revert RebasingInterestInsufficient(_tokenId, _amount, accruedAmount);
1✔
337
        }
338

339
        // s.tokenBalances[_tokenId][_tokenId] += _amount;
340
        // s.depositTotal[_tokenId] += _amount;
341
        _internalMint(LibAdmin._getSystemId(), _tokenId, _amount);
2✔
342
    }
343
}
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