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

GoodDollar / GoodProtocol / 3881987600

pending completion
3881987600

push

github

sirpy
fix: buyandbridge test

859 of 1096 branches covered (78.38%)

Branch coverage included in aggregate %.

2563 of 2779 relevant lines covered (92.23%)

93.13 hits per line

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

15.04
/contracts/staking/FuseStakingV3.sol
1
// SPDX-License-Identifier: MIT
2

3
pragma solidity >=0.8;
4
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
5
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
6
import "@openzeppelin/contracts-upgradeable/utils/math/SafeMathUpgradeable.sol";
7

8
import "../Interfaces.sol";
9
import "../ubi/UBIScheme.sol";
10

11
// import "hardhat/console.sol";
12

13
interface IConsensus {
14
        /**
15
         * @dev delegate to a validator
16
         * @param _validator the address of the validator msg.sender is delegating to
17
         */
18
        function delegate(address _validator) external payable;
19

20
        /**
21
         * @dev Function to be called when a delegator whishes to withdraw some of his staked funds for a validator
22
         * @param _validator the address of the validator msg.sender has delegating to
23
         * @param _amount the amount msg.sender wishes to withdraw from the contract
24
         */
25
        function withdraw(address _validator, uint256 _amount) external;
26

27
        function delegatedAmount(address _address, address _validator)
28
                external
29
                view
30
                returns (uint256);
31

32
        function stakeAmount(address _address) external view returns (uint256);
33

34
        function delegators(address _validator)
35
                external
36
                view
37
                returns (address[] memory);
38
}
39

40
interface PegSwap {
41
        /**
42
         * @notice exchanges the source token for target token
43
         * @param sourceAmount count of tokens being swapped
44
         * @param source the token that is being given
45
         * @param target the token that is being taken
46
         */
47
        function swap(
48
                uint256 sourceAmount,
49
                address source,
50
                address target
51
        ) external;
52
}
53

54
contract FuseStakingV3 is Initializable, OwnableUpgradeable {
55
        using SafeMathUpgradeable for uint256;
56

57
        mapping(address => uint256) public stakers;
58
        address[] public validators;
59

60
        IConsensus public consensus;
61

62
        Uniswap public uniswap;
63
        IGoodDollar public GD;
64
        UBIScheme public ubischeme;
65
        UniswapFactory public uniswapFactory;
66
        UniswapPair public uniswapPair;
67

68
        uint256 public lastDayCollected; //ubi day from ubischeme
69

70
        uint256 public stakeBackRatio;
71
        uint256 public maxSlippageRatio; //actually its max price impact ratio
72
        uint256 public keeperFeeRatio;
73
        uint256 public RATIO_BASE;
74
        uint256 public communityPoolRatio; //out of G$ bought how much should goto pool
75

76
        uint256 public communityPoolBalance;
77
        uint256 public pendingFuseEarnings; //earnings not  used because of slippage
78

79
        address public USDC;
80
        address public fUSD;
81

82
        bool public paused;
83
        address public guardian;
84

85
        PegSwap pegSwap;
86

87
        event UBICollected(
88
                uint256 indexed currentDay,
89
                uint256 ubi, //G$ sent to ubischeme
90
                uint256 communityPool, //G$ added to pool
91
                uint256 gdBought, //actual G$ we got out of swapping stakingRewards + pendingFuseEarnings
92
                uint256 stakingRewards, //rewards earned since previous collection,
93
                uint256 pendingFuseEarnings, //new balance of fuse pending to be swapped for G$
94
                address keeper,
95
                uint256 keeperGDFee
96
        );
97

98
        /**
99
         * @dev initialize
100
         */
101
        function initialize(address _uniswap, address _gd) public initializer {
102
                __Ownable_init_unchained();
1✔
103
                consensus = IConsensus(address(0x3014ca10b91cb3D0AD85fEf7A3Cb95BCAc9c0f79));
1✔
104
                validators.push(address(0xcb876A393F05a6677a8a029f1C6D7603B416C0A6));
1✔
105
                stakeBackRatio = 33333; //%33
1✔
106
                communityPoolRatio = 33333; //%33
1✔
107
                maxSlippageRatio = 3000; //3%
1✔
108
                keeperFeeRatio = 30; //0.03%
1✔
109
                RATIO_BASE = 100000; //100%
1✔
110
                uniswap = Uniswap(
1✔
111
                        _uniswap == address(0)
112
                                ? 0xE3F85aAd0c8DD7337427B9dF5d0fB741d65EEEB5
113
                                : _uniswap
114
                );
115

116
                GD = IGoodDollar(_gd);
1✔
117
                uniswapFactory = UniswapFactory(uniswap.factory());
1✔
118
                uniswapPair = UniswapPair(uniswapFactory.getPair(uniswap.WETH(), _gd));
1✔
119
                pegSwap = PegSwap(0xdfE016328E7BcD6FA06614fE3AF3877E931F7e0a);
1✔
120
                USDC = address(0x620fd5fa44BE6af63715Ef4E65DDFA0387aD13F5);
1✔
121
                fUSD = address(0x249BE57637D8B013Ad64785404b24aeBaE9B098B);
1✔
122
        }
123

124
        modifier notPaused() {
125
                require(paused == false, "ubi collection is pauased");
×
126
                _;
×
127
        }
128

129
        modifier onlyGuardian() {
130
                require(msg.sender == guardian, "not guardian");
×
131
                _;
×
132
        }
133

134
        function approve() external {
135
                cERC20(fUSD).approve(address(pegSwap), type(uint256).max);
×
136
                cERC20(USDC).approve(address(uniswap), type(uint256).max);
×
137
        }
138

139
        function setUBIScheme(address _ubischeme) public onlyOwner {
140
                ubischeme = UBIScheme(_ubischeme);
1✔
141
        }
142

143
        function stake() public payable returns (bool) {
144
                return stake(address(0));
×
145
        }
146

147
        function stake(address _validator)
148
                public
149
                payable
150
                onlyGuardian
151
                returns (bool)
152
        {
153
                require(msg.value > 0, "stake must be > 0");
×
154
                require(validators.length > 0, "no approved validators");
×
155
                bool found;
×
156
                for (
×
157
                        uint256 i = 0;
158
                        _validator != address(0) && i < validators.length;
159
                        i++
160
                ) {
161
                        if (validators[i] != _validator) {
×
162
                                found = true;
×
163
                                break;
×
164
                        }
165
                }
166
                require(
×
167
                        _validator == address(0) || found,
168
                        "validator not in approved list"
169
                );
170

171
                bool staked = stakeNextValidator(msg.value, _validator);
×
172
                stakers[msg.sender] += msg.value;
×
173
                return staked;
×
174
        }
175

176
        function balanceOf(address _owner) public view returns (uint256) {
177
                return stakers[_owner];
×
178
        }
179

180
        function withdrawAll() public onlyGuardian {
181
                for (uint256 i = 0; i < validators.length; i++) {
×
182
                        uint256 cur = consensus.delegatedAmount(address(this), validators[i]);
×
183
                        if (cur == 0) continue;
×
184
                        undelegateWithCatch(validators[i], cur);
×
185
                }
186
                uint256 effectiveBalance = balance(); //use only undelegated funds
×
187
                pendingFuseEarnings = 0;
×
188
                if (effectiveBalance > 0) {
×
189
                        (bool ok, ) = msg.sender.call{ value: effectiveBalance }("");
×
190
                        require(ok, "transfer failed");
×
191
                }
192
        }
193

194
        function withdraw(uint256 _value) public returns (uint256) {
195
                uint256 effectiveBalance = balance(); //use only undelegated funds
×
196
                uint256 toWithdraw = _value == 0 ? stakers[msg.sender] : _value;
×
197
                uint256 toCollect = toWithdraw;
×
198
                require(
×
199
                        toWithdraw > 0 && toWithdraw <= stakers[msg.sender],
200
                        "invalid withdraw amount"
201
                );
202
                uint256 perValidator = _value.div(validators.length);
×
203
                for (uint256 i = 0; i < validators.length; i++) {
×
204
                        uint256 cur = consensus.delegatedAmount(address(this), validators[i]);
×
205
                        if (cur == 0) continue;
×
206
                        if (cur <= perValidator) {
×
207
                                undelegateWithCatch(validators[i], cur);
×
208
                                toCollect = toCollect.sub(cur);
×
209
                        } else {
210
                                undelegateWithCatch(validators[i], perValidator);
×
211
                                toCollect = toCollect.sub(perValidator);
×
212
                        }
213
                        if (toCollect == 0) break;
×
214
                }
215

216
                effectiveBalance = balance().sub(effectiveBalance); //use only undelegated funds
×
217

218
                // in case some funds where not withdrawn
219
                if (toWithdraw > effectiveBalance) {
×
220
                        toWithdraw = effectiveBalance;
×
221
                }
222

223
                stakers[msg.sender] = stakers[msg.sender].sub(toWithdraw);
×
224
                if (toWithdraw > 0) {
×
225
                        msg.sender.call{ value: toWithdraw }("");
×
226
                }
227
                return toWithdraw;
×
228
        }
229

230
        function stakeNextValidator(uint256 _value, address _validator)
231
                internal
232
                returns (bool)
233
        {
234
                if (validators.length == 0) return false;
×
235
                if (_validator != address(0)) {
×
236
                        consensus.delegate{ value: _value }(_validator);
×
237
                        return true;
×
238
                }
239

240
                uint256 perValidator = (totalDelegated() + _value) / validators.length;
×
241
                uint256 left = _value;
×
242
                for (uint256 i = 0; i < validators.length && left > 0; i++) {
×
243
                        uint256 cur = consensus.delegatedAmount(address(this), validators[i]);
×
244

245
                        if (cur < perValidator) {
×
246
                                uint256 toDelegate = perValidator.sub(cur);
×
247
                                toDelegate = toDelegate < left ? toDelegate : left;
×
248
                                consensus.delegate{ value: toDelegate }(validators[i]);
×
249
                                left = left.sub(toDelegate);
×
250
                        }
251
                }
252

253
                return true;
×
254
        }
255

256
        function addValidator(address _v) public onlyOwner {
257
                validators.push(_v);
×
258
        }
259

260
        function totalDelegated() public view returns (uint256) {
261
                uint256 total = 0;
×
262
                for (uint256 i = 0; i < validators.length; i++) {
×
263
                        uint256 cur = consensus.delegatedAmount(address(this), validators[i]);
×
264
                        total += cur;
×
265
                }
266
                return total;
×
267
        }
268

269
        function removeValidator(address _validator) public onlyOwner {
270
                uint256 delegated = consensus.delegatedAmount(address(this), _validator);
×
271
                if (delegated > 0) {
×
272
                        uint256 prevBalance = balance();
×
273
                        undelegateWithCatch(_validator, delegated);
×
274

275
                        // wasnt withdrawn because validator needs to be taken of active validators
276
                        if (balance() == prevBalance) {
×
277
                                // pendingValidators.push(_validator);
278
                                return;
×
279
                        }
280
                }
281

282
                for (uint256 i = 0; i < validators.length; i++) {
×
283
                        if (validators[i] == _validator) {
×
284
                                if (i < validators.length - 1)
×
285
                                        validators[i] = validators[validators.length - 1];
×
286
                                validators.pop();
×
287
                                break;
×
288
                        }
289
                }
290
        }
291

292
        function collectUBIInterest() public notPaused {
293
                uint256 curDay = ubischeme.currentDay();
×
294
                require(curDay != lastDayCollected, "can collect only once in a ubi cycle");
×
295

296
                uint256 earnings = balance() - pendingFuseEarnings;
×
297
                require(pendingFuseEarnings + earnings > 0, "no earnings to collect");
×
298

299
                lastDayCollected = curDay;
×
300
                uint256 fuseUBI = earnings.mul(RATIO_BASE - stakeBackRatio).div(RATIO_BASE);
×
301
                uint256 stakeBack = earnings - fuseUBI;
×
302

303
                uint256[] memory fuseswapResult = _buyGD(fuseUBI + pendingFuseEarnings); //buy GD with X% of earnings
×
304
                pendingFuseEarnings = fuseUBI + pendingFuseEarnings - fuseswapResult[0];
×
305
                stakeNextValidator(stakeBack, address(0)); //stake back the rest of the earnings
×
306

307
                uint256 gdBought = fuseswapResult[fuseswapResult.length - 1];
×
308
                uint256 keeperFee = gdBought.mul(keeperFeeRatio).div(RATIO_BASE);
×
309
                if (keeperFee > 0) GD.transfer(msg.sender, keeperFee);
×
310
                gdBought -= keeperFee;
×
311
                uint256 communityPoolContribution = gdBought.mul(communityPoolRatio).div(
×
312
                        RATIO_BASE
313
                ); //subtract fee // * ommunityPoolRatio // = G$ after fee * communityPoolRatio%
314

315
                uint256 ubiAfterFeeAndPool = gdBought.sub(communityPoolContribution);
×
316

317
                GD.transfer(address(ubischeme), ubiAfterFeeAndPool); //transfer to ubischeme
×
318
                communityPoolBalance += communityPoolContribution;
×
319

320
                emit UBICollected(
×
321
                        curDay,
322
                        ubiAfterFeeAndPool,
323
                        communityPoolContribution,
324
                        gdBought,
325
                        earnings,
326
                        pendingFuseEarnings,
327
                        msg.sender,
328
                        keeperFee
329
                );
330
        }
331

332
        /**
333
         * @dev internal method to buy GD from fuseswap
334
         * @param _value fuse to be sold
335
         * @return uniswap coversion results uint256[2]
336
         */
337
        function _buyGD(uint256 _value) internal returns (uint256[] memory) {
338
                //buy from uniwasp
339
                require(_value > 0, "buy value should be > 0");
×
340
                (uint256 maxFuse, uint256 fuseGDOut) = calcMaxFuseWithPriceImpact(_value);
×
341
                (uint256 maxFuseUSDC, uint256 usdcGDOut) = calcMaxFuseUSDCWithPriceImpact(
×
342
                        _value
343
                );
344
                address[] memory path;
×
345
                if (maxFuse >= maxFuseUSDC) {
×
346
                        path = new address[](2);
×
347
                        path[1] = address(GD);
×
348
                        path[0] = uniswap.WETH();
×
349
                        return
×
350
                                uniswap.swapExactETHForTokens{ value: maxFuse }(
351
                                        (fuseGDOut * 95) / 100,
352
                                        path,
353
                                        address(this),
354
                                        block.timestamp
355
                                );
356
                } else {
357
                        (uint256 usdcAmount, uint256 usedFuse) = _buyUSDC(maxFuseUSDC);
×
358
                        path = new address[](2);
×
359
                        path[1] = address(GD);
×
360
                        path[0] = USDC;
×
361

362
                        uint256[] memory result = uniswap.swapExactTokensForTokens(
×
363
                                usdcAmount,
364
                                (usdcGDOut * 95) / 100,
365
                                path,
366
                                address(this),
367
                                block.timestamp
368
                        );
369
                        //buyGD should return how much fuse was used in [0] and how much G$ we got in [1]
370
                        result[0] = usedFuse;
×
371
                        return result;
×
372
                }
373
        }
374

375
        /**
376
         * @dev internal method to buy USDC via fuse->fusd
377
         * @param _fuseIn fuse to be sold
378
         * @return usdcAmount and usedFuse how much usdc we got and how much fuse was used
379
         */
380

381
        function _buyUSDC(uint256 _fuseIn)
382
                internal
383
                returns (uint256 usdcAmount, uint256 usedFuse)
384
        {
385
                //buy from uniwasp
386
                require(_fuseIn > 0, "buy value should be > 0");
×
387
                UniswapPair uniswapFUSEfUSDPair = UniswapPair(
×
388
                        uniswapFactory.getPair(uniswap.WETH(), fUSD)
389
                ); //fusd is pegged 1:1 to usdc
390
                (uint256 r_fuse, uint256 r_fusd, ) = uniswapFUSEfUSDPair.getReserves();
×
391

392
                (uint256 maxFuse, uint256 tokenOut) = calcMaxTokenWithPriceImpact(
×
393
                        r_fuse,
394
                        r_fusd,
395
                        _fuseIn
396
                ); //expect r_token to be in 18 decimals
397

398
                address[] memory path = new address[](2);
×
399
                path[1] = fUSD;
×
400
                path[0] = uniswap.WETH();
×
401
                uint256[] memory result = uniswap.swapExactETHForTokens{ value: maxFuse }(
×
402
                        (tokenOut * 95) / 100,
403
                        path,
404
                        address(this),
405
                        block.timestamp
406
                );
407

408
                pegSwap.swap(result[1], fUSD, USDC);
×
409
                usedFuse = result[0];
×
410
                usdcAmount = result[1] / 1e12; //convert fusd from 1e18 to usdc 1e6
×
411
        }
412

413
        function calcMaxFuseWithPriceImpact(uint256 _value)
414
                public
415
                view
416
                returns (uint256 fuseAmount, uint256 tokenOut)
417
        {
418
                (uint256 r_fuse, uint256 r_gd, ) = UniswapPair(
2✔
419
                        uniswapFactory.getPair(uniswap.WETH(), address(GD))
420
                ).getReserves();
421

422
                return calcMaxTokenWithPriceImpact(r_fuse, r_gd, _value);
2✔
423
        }
424

425
        function calcMaxFuseUSDCWithPriceImpact(uint256 _value)
426
                public
427
                view
428
                returns (uint256 maxFuse, uint256 gdOut)
429
        {
430
                UniswapPair uniswapFUSEfUSDPair = UniswapPair(
3✔
431
                        uniswapFactory.getPair(uniswap.WETH(), fUSD)
432
                ); //fusd is pegged 1:1 to usdc
433
                UniswapPair uniswapGDUSDCPair = UniswapPair(
3✔
434
                        uniswapFactory.getPair(address(GD), USDC)
435
                );
436
                (uint256 rg_gd, uint256 rg_usdc, ) = uniswapGDUSDCPair.getReserves();
3✔
437
                (uint256 r_fuse, uint256 r_fusd, ) = uniswapFUSEfUSDPair.getReserves();
3✔
438
                //fusd is 1e18 so to keep in original 1e18 precision we first multiply by 1e18
439
                uint256 fusdPriceInFuse = r_fuse.mul(1e18).div(r_fusd);
3✔
440
                // console.log(
441
                //         "rgd: %s rusdc:%s usdcPriceInFuse: %s",
442
                //         rg_gd,
443
                //         rg_usdc,
444
                //         fusdPriceInFuse
445
                // );
446
                // console.log("rfuse: %s rusdc:%s", r_fuse, r_fusd);
447

448
                //how many fusd we can get for fuse
449
                //value and usdPriceInFuse are in 1e18, we mul by 1e18 to keep 18 decimals precision
450
                uint256 fuseValueInfUSD = _value.mul(1e18).div(fusdPriceInFuse);
3✔
451
                // console.log("fuse fusd value: %s", fuseValueInfUSD);
452

453
                (uint256 maxUSDC, uint256 tokenOut) = calcMaxTokenWithPriceImpact(
3✔
454
                        rg_usdc * 1e12,
455
                        rg_gd,
456
                        fuseValueInfUSD
457
                ); //expect r_token to be in 18 decimals
458
                // console.log("max USDC: %s", maxUSDC);
459
                gdOut = tokenOut;
3✔
460
                maxFuse = maxUSDC.mul(fusdPriceInFuse).div(1e18); //both are in 1e18 precision, div by 1e18 to keep precision
3✔
461
        }
462

463
        /**
464
         * uniswap amountOut helper
465
         */
466
        function getAmountOut(
467
                uint256 _amountIn,
468
                uint256 _reserveIn,
469
                uint256 _reserveOut
470
        ) internal pure returns (uint256 amountOut) {
471
                uint256 amountInWithFee = _amountIn * 997;
6✔
472
                uint256 numerator = amountInWithFee * _reserveOut;
6✔
473
                uint256 denominator = _reserveIn * 1000 + amountInWithFee;
6✔
474
                amountOut = numerator / denominator;
6✔
475
        }
476

477
        /**
478
         * @dev use binary search to find quantity that will result with price impact < maxPriceImpactRatio
479
         */
480
        function calcMaxTokenWithPriceImpact(
481
                uint256 r_token,
482
                uint256 r_gd,
483
                uint256 _value
484
        ) public view returns (uint256 maxToken, uint256 tokenOut) {
485
                maxToken = (r_token * maxSlippageRatio) / RATIO_BASE;
6✔
486
                maxToken = maxToken < _value ? maxToken : _value;
6✔
487
                tokenOut = getAmountOut(maxToken, r_token, r_gd);
6✔
488
                // uint256 start = 0;
489
                // uint256 end = _value.div(1e18); //save iterations by moving precision to whole Fuse quantity
490
                // // uint256 curPriceWei = uint256(1e18).mul(r_gd) / r_token; //uniswap quote  formula UniswapV2Library.sol
491
                // uint256 gdForQuantity = getAmountOut(1e18, r_token, r_gd);
492
                // uint256 priceForQuantityWei = rdiv(1e18, gdForQuantity.mul(1e16)).div(
493
                //         1e9
494
                // );
495
                // uint256 maxPriceWei = priceForQuantityWei
496
                // .mul(RATIO_BASE.add(maxSlippageRatio))
497
                // .div(RATIO_BASE);
498
                // // console.log(
499
                // //         "curPrice: %s, maxPrice %s",
500
                // //         priceForQuantityWei,
501
                // //         maxPriceWei
502
                // // );
503
                // fuseAmount = _value;
504
                // tokenOut;
505
                // //Iterate while start not meets end
506
                // while (start <= end) {
507
                //         // Find the mid index
508
                //         uint256 midQuantityWei = start.add(end).mul(1e18).div(2); //restore quantity precision
509
                //         if (midQuantityWei == 0) break;
510
                //         gdForQuantity = getAmountOut(midQuantityWei, r_token, r_gd);
511
                //         priceForQuantityWei = rdiv(midQuantityWei, gdForQuantity.mul(1e16))
512
                //         .div(1e9);
513
                //         // console.log(
514
                //         //         "gdForQuantity: %s, priceForQuantity: %s, midQuantity: %s",
515
                //         //         gdForQuantity,
516
                //         //         priceForQuantityWei,
517
                //         //         midQuantityWei
518
                //         // );
519
                //         if (priceForQuantityWei <= maxPriceWei) {
520
                //                 start = midQuantityWei.div(1e18) + 1; //reduce precision to whole quantity div 1e18
521
                //                 fuseAmount = midQuantityWei;
522
                //                 tokenOut = gdForQuantity;
523
                //         } else end = midQuantityWei.div(1e18) - 1; //reduce precision to whole quantity div 1e18
524
                // }
525
        }
526

527
        function undelegateWithCatch(address _validator, uint256 _amount)
528
                internal
529
                returns (bool)
530
        {
531
                try consensus.withdraw(_validator, _amount) {
×
532
                        return true;
×
533
                } catch Error(
534
                        string memory /*reason*/
535
                ) {
536
                        // This is executed in case
537
                        // revert was called inside getData
538
                        // and a reason string was provided.
539
                        return false;
×
540
                } catch (
541
                        bytes memory /*lowLevelData*/
542
                ) {
543
                        // This is executed in case revert() was used
544
                        // or there was a failing assertion, division
545
                        // by zero, etc. inside getData.
546
                        return false;
×
547
                }
548
        }
549

550
        function balance() internal view returns (uint256) {
551
                return payable(address(this)).balance;
×
552
        }
553

554
        function setPaused(bool _paused) external onlyGuardian {
555
                paused = _paused;
×
556
        }
557

558
        function setGuardian(address _guardian) external onlyGuardian {
559
                guardian = _guardian;
×
560
        }
561

562
        function collectCommunityPool(address _to, uint256 amount)
563
                external
564
                onlyGuardian
565
        {
566
                communityPoolBalance -= amount;
×
567
                GD.transfer(_to, amount);
×
568
        }
569

570
        receive() external payable {}
571
}
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