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

NexusMutual / smart-contracts / #778

23 Jul 2024 01:54PM UTC coverage: 87.593% (+4.3%) from 83.289%
#778

push

MilGard91
Run compilation in the storage layout extraction script if invoked from cli (thx @fvictorio)

1023 of 1274 branches covered (80.3%)

Branch coverage included in aggregate %.

2987 of 3304 relevant lines covered (90.41%)

170.73 hits per line

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

95.92
/contracts/modules/staking/StakingNFTDescriptor.sol
1
// SPDX-License-Identifier: GPL-3.0-only
2

3
pragma solidity ^0.8.0;
4

5
import "@openzeppelin/contracts-v4/utils/Strings.sol";
6
import "@openzeppelin/contracts-v4/utils/Base64.sol";
7
import "../../interfaces/IStakingNFT.sol";
8
import "../../interfaces/IStakingNFTDescriptor.sol";
9
import "../../interfaces/IStakingPool.sol";
10
import "../../interfaces/IStakingPoolFactory.sol";
11
import "../../libraries/DateTime.sol";
12
import "../../libraries/FloatingPoint.sol";
13
import "../../libraries/StakingPoolLibrary.sol";
14

15
contract StakingNFTDescriptor is IStakingNFTDescriptor {
16
  using Strings for uint;
17
  using DateTime for uint;
18

19
  uint public constant TRANCHE_DURATION = 91 days;
20
  uint public constant MAX_ACTIVE_TRANCHES = 8;
21
  uint public constant ONE_NXM = 1 ether;
22
  uint public constant NXM_DECIMALS = 18;
23

24
  function tokenURI(uint tokenId) public view returns (string memory) {
25
    (string memory description, StakeData memory stakeData) = buildDescription(tokenId);
3✔
26
    string memory image = Base64.encode(bytes(generateSVGImage(stakeData)));
3✔
27

28
    return string(
3✔
29
      abi.encodePacked(
30
        "data:application/json;base64,",
31
        Base64.encode(
32
          bytes(
33
            abi.encodePacked(
34
              '{"name":"', IStakingNFT(msg.sender).name(), '",',
35
              '"description":"', description, '",',
36
              '"image": "', "data:image/svg+xml;base64,", image,
37
              '"}'
38
            )
39
          )
40
        )
41
      )
42
    );
43
  }
44

45
  function buildDescription(uint tokenId) public view returns (string memory description, StakeData memory stakeData) {
46
    uint poolId = IStakingNFT(msg.sender).stakingPoolOf(tokenId);
3✔
47
    address stakingPoolFactory = IStakingNFT(msg.sender).stakingPoolFactory();
3✔
48
    address stakingPool = StakingPoolLibrary.getAddress(stakingPoolFactory, poolId);
3✔
49

50
    // Check if token exists
51
    (string memory depositInfo, uint totalStake, uint pendingRewards) = getActiveDeposits(
3✔
52
      tokenId,
53
      IStakingPool(stakingPool)
54
    );
55

56
    // Add pool info
57
    description = append(
3✔
58
      "This NFT represents a deposit into staking pool: ",
59
      uint(uint160(stakingPool)).toHexString()
60
    );
61
    description = appendWithNewline(description, "Pool ID: ", poolId.toString());
3✔
62

63
    // No active deposits, assume it has expired
64
    if (totalStake == 0) {
3✔
65
      description = appendWithNewline(description, "Deposit has expired!");
1✔
66
      return (description, StakeData(poolId, 0, tokenId));
1✔
67
    }
68

69
    // Add deposit info
70
    description = appendWithNewline(description, "Staked amount: ", FloatingPoint.toFloat(totalStake, NXM_DECIMALS), " NXM");
2✔
71
    description = appendWithNewline(description, "Pending rewards: ", FloatingPoint.toFloat(pendingRewards, NXM_DECIMALS), " NXM");
2✔
72
    description = appendWithNewline(description, "Active deposits: ", depositInfo);
2✔
73

74
    return (description, StakeData(poolId, totalStake, tokenId));
2✔
75
  }
76

77
  function getActiveDeposits(
78
    uint tokenId,
79
    IStakingPool stakingPool
80
  ) public view returns (
81
    string memory depositInfo,
82
    uint totalStake,
83
    uint pendingRewards
84
  ) {
85

86
    uint activeStake = stakingPool.getActiveStake();
3✔
87
    uint stakeSharesSupply = stakingPool.getStakeSharesSupply();
3✔
88

89
    // Get total stake from each active tranche
90
    for (uint i = 0; i < MAX_ACTIVE_TRANCHES; i++) {
3✔
91
      // get deposit
92
      (uint lastAccNxmPerRewardShare, uint _pendingRewards, uint _stakeShares, uint _rewardsShares) =
24✔
93
      stakingPool.getDeposit(tokenId, (block.timestamp / TRANCHE_DURATION) + i);
94

95
      // no active stake, skip this tranche
96
      if (_rewardsShares == 0) {
24✔
97
        continue;
21✔
98
      }
99

100
      string memory dateString;
3✔
101
      {
3✔
102
        // calculate days left until stake expires
103
        uint secondsLeftInTranche = (TRANCHE_DURATION - (block.timestamp % TRANCHE_DURATION));
3✔
104
        (uint year, uint month, uint day) = (block.timestamp + (secondsLeftInTranche + (i * TRANCHE_DURATION))).timestampToDate();
3✔
105
        dateString = string(abi.encodePacked(month.getMonthString(), " ", addZeroPrefix(day), " ", year.toString()));
3✔
106
      }
107

108
      uint stake = (activeStake * _stakeShares) / stakeSharesSupply;
3✔
109

110
      depositInfo = appendWithNewline(
3✔
111
        depositInfo,
112
        " -",
113
        append(FloatingPoint.toFloat(stake, NXM_DECIMALS), " NXM will expire at tranche: ", uint((block.timestamp / TRANCHE_DURATION) + i).toString()),
114
        append(" (", dateString, ")")
115
      );
116

117
      // update pending rewards
118
      uint newRewardPerShare = stakingPool.getAccNxmPerRewardsShare() - lastAccNxmPerRewardShare;
3✔
119
      pendingRewards += _pendingRewards + (newRewardPerShare * _rewardsShares) / ONE_NXM;
3✔
120

121
      // update total stake
122
      totalStake += stake;
3✔
123
    }
124
    return (depositInfo, totalStake, pendingRewards);
3✔
125
  }
126

127

128
  function generateSVGImage(StakeData memory stakeDescription) public pure returns (bytes memory) {
129
    return abi.encodePacked(
3✔
130
      string(
131
        abi.encodePacked(
132
          '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 290 500"><defs><style> .cls-1 { fill: none; } .cls-3 { clip-path: url(#cp1); } .cls-4 { opacity: .04; } .cls-5 { fill: #002332; } .cls-6, .cls-7, .cls-8, .cls-9 { fill: #fff; } .cls-7 { font-size: 18px; } .cls-7, .cls-8 { font-family: ArialMT, Arial, sans-serif; } .cls-9 { clip-path: url(#cp2); } .cls-8, .cls-9 { font-size: 13px; }',
133
          '</style><clipPath id="cp1"><rect class="cls-1" width="290" height="500" rx="32.74" ry="32.74"/></clipPath><clipPath id="cp2"><rect class="cls-1" x="-382.18" y="-90" width="968.85" height="971"/></clipPath></defs><g><rect class="cls-5" y="0" width="290" height="500" rx="32.74" ry="32.74"/><g><path class="cls-6" d="m137.65,344.29c5.97-5.76,6.14-15.26.38-21.23s-15.26-6.14-21.23-.38-6.14,15.26-.38,21.23c5.76,5.97,15.26,6.14,21.23.38Z"/><path class="cls-6" d="m55.85,382.84c5.97-5.76,6.14-15.26.38-21.23s-15.26-6.14-21.23-.38c-5.97,5.76-6.14,15.26-.38,21.23,5.76,5.97,15.26,6.14,21.23.38Z"/><path class="cls-6" d="m95.2,443.55c-5.78-5.95-15.28-6.09-21.23-.31-5.92,5.75-6.09,15.2-.38,21.16h0c5.7,6.02,15.2,6.28,21.23.58,6.02-5.7,6.28-15.2.58-21.23-.06-.07-.13-.14-.2-.2Z"/><path class="cls-6" d="m176.38,404.39c-5.76-5.97-15.26-6.14-21.23-.38-5.97,5.76-6.14,15.26-.38,21.23,5.76,5.97,15.26,6.14,21.23.38,5.97-5.76,6.14-15.26.38-21.23Z"/><path class="cls-6" d="m170.2,369.41c2.85-2.87,4.35-6.82,4.13-10.86v-.11c-.68-12.34-15.65-18.07-24.39-9.33l-14.15,14.4c-4.53,4.53-15,5.22-20.37-.16l-33.84-34.24c-2.69-2.78-6.4-4.35-10.27-4.35-12.74,0-19.11,15.4-10.1,24.41l14.27,13.77c5.19,5.65,4.62,15.6,0,20.22l-34.28,34.35c-2.64,2.64-4.14,6.21-4.19,9.94v.32c-.15,12.82,15.34,19.34,24.4,10.28l14.24-14.31c4.96-4.96,15.27-5.01,20.46.18l33.7,33.9c2.79,2.79,6.6,4.3,10.53,4.18h.05c12.53-.37,18.55-15.54,9.69-24.4l-13.61-13.75c-5.05-5.05-5.43-14.79-.03-20.19l33.77-34.25Zm-53.42,35.38c-6.27,6.05-16.26,5.87-22.32-.4-6.05-6.27-5.87-16.26.4-22.32,6.27-6.05,16.26-5.87,22.32.4,6.05,6.27,5.87,16.26-.4,22.32Z"/></g><rect class="cls-1" width="290" height="500" rx="32.74" ry="32.74"/><g class="cls-4"><g class="cls-3"><g class="cls-9"><path class="cls-6" d="m166.21,199.57L0,31.41v468.59h279.42c2.06-8.38,5.24-16.47,9.58-23.93V207.76c-32.29,22.54-91.17,23.44-122.79-8.18Zm8.78,267.25c-40.45,39.03-104.88,37.88-143.92-2.57-39.03-40.45-37.88-104.88,2.57-143.92,40.45-39.03,104.88-37.88,143.92,2.57,39.03,40.45,37.88,104.88-2.57,143.92Z"/><path class="cls-6" d="m290,0h-144.24c-1.92,26.48,6.99,53.64,26.88,74.26,31.32,32.46,79.85,38.32,117.36,17.07V0Z"/></g></g></g><g><polygon class="cls-6" points="221.16 46.69 217.76 55.77 216.64 55.77 213.24 46.69 213.24 55.77 210.67 55.77 210.67 43.28 214.27 43.28 217.2 50.64 220.13 43.28 223.75 43.28 223.75 55.77 221.16 55.77 221.16 46.69"/><rect class="cls-6" x="265.25" y="42.99" width="2.43" height="12.78"/><g><path class="cls-6" d="m204.34,47.11c1.18-1.13,1.22-3,.08-4.18-1.15-1.18-3-1.22-4.18-.07-1.16,1.12-1.19,3-.09,4.16,1.13,1.18,3.01,1.22,4.19.1"/><path class="cls-6" d="m184.16,54.75c1.13,1.18,3,1.22,4.18.08,1.18-1.15,1.22-3,.07-4.18-1.12-1.16-3-1.19-4.16-.09-1.18,1.13-1.22,3.01-.1,4.19"/><path class="cls-6" d="m176.56,34.43c-1.18,1.13-1.22,3-.08,4.18,1.15,1.18,3,1.22,4.18.07,1.16-1.12,1.19-3,.09-4.16-1.13-1.18-3.01-1.22-4.19-.1"/><path class="cls-6" d="m196.86,26.91c-1.13-1.18-3-1.22-4.18-.08-1.18,1.15-1.22,3-.07,4.18,1.12,1.16,3,1.19,4.16.09,1.18-1.13,1.22-3.01.1-4.19"/><path class="cls-6" d="m192.67,43.01c-1.24,1.18-3.22,1.14-4.41-.1-1.16-1.22-1.12-3.2.09-4.37,1.24-1.2,3.19-1.17,4.4.08,1.2,1.24,1.17,3.21-.08,4.4m10.52-6.97c.56-.57.86-1.34.81-2.14v-.02c-.13-2.43-3.08-3.56-4.81-1.84l-2.79,2.84c-.89.89-2.95,1.03-4.01-.03l-6.67-6.75c-.53-.55-1.26-.86-2.02-.86-2.51,0-3.76,3.03-1.99,4.81l2.81,2.71c1.02,1.11.91,3.07,0,3.98l-6.75,6.77c-.52.52-.82,1.22-.82,1.96v.06c-.03,2.53,3.02,3.81,4.81,2.03l2.81-2.82c.98-.98,3.01-.99,4.03.04l6.64,6.68c.55.55,1.3.85,2.08.82h0c2.47-.07,3.65-3.06,1.91-4.81l-2.68-2.71c-.99-.99-1.07-2.91,0-3.98l6.65-6.75Z"/></g><g><polygon class="cls-6" points="213.39 30.52 213.39 38.85 210.67 38.85 210.67 26.08 213.47 26.08 219.39 34.1 219.39 26.08 222.11 26.08 222.11 38.85 219.48 38.85 213.39 30.52"/><path class="cls-6" d="m226.49,33.35h4.56c-.06-.88-.67-2.05-2.28-2.05-1.51,0-2.16,1.13-2.28,2.05m2.28-3.98c2.76,0,4.64,2.07,4.64,5.09v.54h-6.88c.15,1.17,1.09,2.14,2.66,2.14.86,0,1.88-.34,2.47-.92l1.07,1.57c-.92.84-2.37,1.28-3.81,1.28-2.82,0-4.94-1.9-4.94-4.86,0-2.68,1.97-4.85,4.79-4.85"/><polygon class="cls-6" points="238.95 35.73 236.82 38.85 234.12 38.85 237.4 34.1 234.31 29.6 237.03 29.6 238.95 32.45 240.85 29.6 243.57 29.6 240.46 34.1 243.78 38.85 241.06 38.85 238.95 35.73"/><path class="cls-6" d="m251.23,37.68c-.63.71-1.74,1.4-3.26,1.4-2.03,0-2.99-1.11-2.99-2.91v-6.57h2.43v5.61c0,1.28.67,1.7,1.7,1.7.94,0,1.69-.52,2.11-1.05v-6.26h2.43v9.25h-2.43v-1.17Z"/><path class="cls-6" d="m256.32,35.94c.67.63,2.05,1.26,3.2,1.26,1.05,0,1.55-.42,1.55-1,0-.63-.84-.8-1.86-1-1.59-.31-3.64-.71-3.64-2.93,0-1.55,1.36-2.91,3.79-2.91,1.57,0,2.82.54,3.74,1.26l-.98,1.67c-.56-.59-1.61-1.07-2.74-1.07-.88,0-1.46.4-1.46.92s.73.69,1.76.9c1.59.31,3.72.75,3.72,3.06,0,1.69-1.44,2.97-4,2.97-1.61,0-3.16-.54-4.14-1.44l1.05-1.7Z"/></g><path class="cls-6" d="m232.28,54.6c-.63.71-1.74,1.4-3.26,1.4-2.03,0-2.99-1.11-2.99-2.91v-6.57h2.43v5.61c0,1.28.67,1.7,1.7,1.7.94,0,1.69-.52,2.11-1.05v-6.26h2.43v9.25h-2.43v-1.17Z"/><path class="cls-6" d="m237.76,53.47v-4.83h-1.53v-2.13h1.53v-2.53h2.45v2.53h1.88v2.13h-1.88v4.18c0,.57.31,1.02.84,1.02.36,0,.71-.13.84-.27l.52,1.84c-.36.33-1.02.59-2.03.59-1.7,0-2.62-.88-2.62-2.53"/><path class="cls-6" d="m250,54.6c-.63.71-1.74,1.4-3.26,1.4-2.03,0-2.99-1.11-2.99-2.91v-6.57h2.43v5.61c0,1.28.67,1.7,1.7,1.7.94,0,1.69-.52,2.11-1.05v-6.26h2.43v9.25h-2.43v-1.17Z"/><path class="cls-6" d="m260.4,52.4c-.4-.54-1.17-.8-1.95-.8-.96,0-1.74.5-1.74,1.4s.78,1.36,1.74,1.36c.79,0,1.55-.27,1.95-.8v-1.15Zm0,2.41c-.63.75-1.72,1.19-2.93,1.19-1.48,0-3.22-1-3.22-3.06s1.74-2.95,3.22-2.95c1.23,0,2.32.38,2.93,1.13v-1.28c0-.94-.8-1.55-2.03-1.55-1,0-1.92.36-2.7,1.09l-.92-1.63c1.13-1,2.59-1.46,4.04-1.46,2.11,0,4.04.84,4.04,3.5v5.98h-2.43v-.96Z"/></g> <text class="cls-7" x="7%" y="25%"><tspan>Staking Info</tspan></text> <text class="cls-8" x="7%" y="35%"><tspan>Pool ID:</tspan></text> <text class="cls-8" x="7%" y="40%"><tspan>Stake:</tspan></text> <text class="cls-8" x="7%" y="45%"><tspan>NFT ID:</tspan></text>',
134
          '<text class="cls-8" x="93%" y="35%" text-anchor="end"><tspan>', stakeDescription.poolId.toString(), '</tspan></text>',
135
          '<text class="cls-8" x="93%" y="40%" text-anchor="end"><tspan>', FloatingPoint.toFloat(stakeDescription.stakeAmount, NXM_DECIMALS), ' NXM</tspan></text>',
136
          '<text class="cls-8" x="93%" y="45%" text-anchor="end"><tspan>', stakeDescription.tokenId.toString(), '</tspan></text></g></svg>'
137
        )
138
      )
139
    );
140
  }
141

142
  // If value is single digit, add a zero prefix
143
  function addZeroPrefix(uint256 value) public pure returns (string memory) {
144
    if (value < 10) {
3!
145
      return string(abi.encodePacked("0", value.toString()));
×
146
    }
147
    return value.toString();
3✔
148
  }
149

150
  function append(string memory a, string memory b) internal pure returns (string memory) {
151
    return string(abi.encodePacked(a, b));
3✔
152
  }
153

154
  function append(string memory a, string memory b, string memory c) internal pure returns (string memory) {
155
    return string(abi.encodePacked(a, b, c));
6✔
156
  }
157

158
  function appendWithNewline(string memory a, string memory b) internal pure returns (string memory) {
159
    return string(abi.encodePacked(a, "\\n", b));
1✔
160
  }
161

162
  function appendWithNewline(string memory a, string memory b, string memory c) internal pure returns (string memory) {
163
    return string(abi.encodePacked(a, "\\n", b, c));
5✔
164
  }
165

166
  function appendWithNewline(string memory a, string memory b, string memory c, string memory d) internal pure returns (string memory) {
167
    return string(abi.encodePacked(a, "\\n", b, c, d));
7✔
168
  }
169

170
  function toFloat(uint number, uint decimals) public pure returns (string memory) {
171
    return FloatingPoint.toFloat(number, decimals);
124✔
172
  }
173
}
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