@@ -11,44 +11,120 @@ import {Ownable2Step} from "@oz/access/Ownable2Step.sol";
1111/**
1212 * @title CoinIssuer
1313 * @author Aztec Labs
14- * @notice A contract that allows minting of coins at a maximum fixed rate
14+ * @notice A contract that allows minting of coins at a maximum percentage rate per year using discrete annual budgets
15+ *
16+ * This contract uses a discrete annual budget model:
17+ * - Years are fixed periods from deployment:
18+ * - year 0 = [deployment, deployment + 365d)
19+ * - year 1 = [deployment + 365d, deployment + (2) * 365d)
20+ * - ...
21+ * - year n = [deployment + 365d * n, deployment + (n + 1) * 365d)
22+ * - Each year's budget is calculated at the start of that year based on the actual supply at that moment
23+ * - Budget = totalSupply() × NOMINAL_ANNUAL_PERCENTAGE_CAP / 1e18
24+ * - Unused budget from year N is LOST when year N+1 begins (use-it-or-lose-it)
25+ *
26+ * Rate semantics: If the full budget is minted every year, the effective annual inflation rate equals
27+ * NOMINAL_ANNUAL_PERCENTAGE_CAP. For example, setting the rate to 0.10e18 (10%) and fully minting each
28+ * year will result in supply growing by exactly 10% annually: supply(year N) = supply(year 0) × (1.10)^N
29+ *
30+ * Partial minting: If less than the full budget is minted in year N, the remaining allowance is lost
31+ * at the year N→N+1 boundary. Year N+1's budget is calculated based on the actual supply at the start
32+ * of year N+1, which reflects only what was actually minted.
33+ *
34+ * @dev The NOMINAL_ANNUAL_PERCENTAGE_CAP is in e18 precision where 1e18 = 100%
35+ *
36+ * @dev The token MUST have a non-zero initial supply at deployment, or an alternative way to mint the token.
1537 */
1638contract CoinIssuer is ICoinIssuer , Ownable {
1739 IMintableERC20 public immutable ASSET;
18- uint256 public immutable RATE ;
19- uint256 public timeOfLastMint ;
40+ uint256 public immutable NOMINAL_ANNUAL_PERCENTAGE_CAP ;
41+ uint256 public immutable DEPLOYMENT_TIME ;
2042
21- constructor (IMintableERC20 _asset , uint256 _rate , address _owner ) Ownable (_owner) {
43+ // Note that the state variables below are "cached":
44+ // they are only updated when minting after a year boundary.
45+ uint256 public cachedBudgetYear;
46+ uint256 public cachedBudget;
47+
48+ constructor (IMintableERC20 _asset , uint256 _annualPercentage , address _owner ) Ownable (_owner) {
2249 ASSET = _asset;
23- RATE = _rate;
24- timeOfLastMint = block .timestamp ;
50+ NOMINAL_ANNUAL_PERCENTAGE_CAP = _annualPercentage;
51+ DEPLOYMENT_TIME = block .timestamp ;
52+
53+ cachedBudgetYear = 0 ;
54+ cachedBudget = _getNewBudget ();
55+
56+ emit BudgetReset (0 , cachedBudget);
2557 }
2658
2759 function acceptTokenOwnership () external override (ICoinIssuer) onlyOwner {
2860 Ownable2Step (address (ASSET)).acceptOwnership ();
2961 }
3062
3163 /**
32- * @notice Mint tokens up to the `mintAvailable` limit
33- * Beware that the mintAvailable will be reset to 0, and not just
34- * reduced by the amount minted.
64+ * @notice Mint `_amount` tokens to `_to`
65+ *
66+ * @dev The `_amount` must be within the `cachedBudget`
3567 *
3668 * @param _to - The address to receive the funds
3769 * @param _amount - The amount to mint
3870 */
3971 function mint (address _to , uint256 _amount ) external override (ICoinIssuer) onlyOwner {
40- uint256 maxMint = mintAvailable ();
41- require (_amount <= maxMint, Errors.CoinIssuer__InsufficientMintAvailable (maxMint, _amount));
42- timeOfLastMint = block .timestamp ;
72+ // Update state if we've crossed into a new year (will reset budget and forfeit unused amount)
73+ _updateBudgetIfNeeded ();
74+
75+ require (_amount <= cachedBudget, Errors.CoinIssuer__InsufficientMintAvailable (cachedBudget, _amount));
76+ cachedBudget -= _amount;
77+
4378 ASSET.mint (_to, _amount);
4479 }
4580
4681 /**
47- * @notice The amount of funds that is available for "minting"
82+ * @notice The amount of funds that is available for "minting" in the current year
83+ * If we've crossed into a new year since the last mint, returns the fresh budget
84+ * for the new year based on current supply.
4885 *
4986 * @return The amount mintable
5087 */
5188 function mintAvailable () public view override (ICoinIssuer) returns (uint256 ) {
52- return RATE * (block .timestamp - timeOfLastMint);
89+ uint256 currentYear = _yearSinceGenesis ();
90+
91+ // Until the budget is stale, return the cached budget
92+ if (cachedBudgetYear >= currentYear) {
93+ return cachedBudget;
94+ }
95+
96+ // Crossed into new year(s): compute fresh budget
97+ return _getNewBudget ();
98+ }
99+
100+ /**
101+ * @notice Internal function to update year and budget when crossing year boundaries
102+ *
103+ * @dev If multiple years have passed without minting, jumps directly to current year
104+ * and all intermediate years' budgets are lost
105+ */
106+ function _updateBudgetIfNeeded () private {
107+ uint256 currentYear = _yearSinceGenesis ();
108+ // If the budget is for the past, update the budget.
109+ if (cachedBudgetYear < currentYear) {
110+ cachedBudgetYear = currentYear;
111+ cachedBudget = _getNewBudget ();
112+
113+ emit BudgetReset (currentYear, cachedBudget);
114+ }
115+ }
116+
117+ /**
118+ * @notice Internal function to compute the current year since genesis
119+ */
120+ function _yearSinceGenesis () private view returns (uint256 ) {
121+ return (block .timestamp - DEPLOYMENT_TIME) / 365 days ;
122+ }
123+
124+ /**
125+ * @notice Internal function to compute a fresh budget
126+ */
127+ function _getNewBudget () private view returns (uint256 ) {
128+ return ASSET.totalSupply () * NOMINAL_ANNUAL_PERCENTAGE_CAP / 1e18 ;
53129 }
54130}
0 commit comments