Skip to content

minter: Add inflation bounds to inflation adjustment algorithm #645

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: delta
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ jobs:
name: ${{ github.event.repository.name }}
token: ${{ secrets.CI_CODECOV_TOKEN }}

- name: Enforce test coverage threshold
run: yarn test:coverage:check

editorconfig:
name: Run editorconfig checker
runs-on: ubuntu-latest
Expand Down
10 changes: 10 additions & 0 deletions contracts/token/IMinter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ interface IMinter {

function currentMintedTokens() external view returns (uint256);

function inflation() external view returns (uint256);

function inflationCeiling() external view returns (uint256);

function inflationFloor() external view returns (uint256);

function inflationChange() external view returns (uint256);

function targetBondingRate() external view returns (uint256);

// Public functions
function getController() external view returns (IController);
}
81 changes: 75 additions & 6 deletions contracts/token/Minter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ contract Minter is Manager, IMinter {

// Per round inflation rate
uint256 public inflation;
// Target maximum inflation rate
uint256 public inflationCeiling;
// Target minimum inflation rate
uint256 public inflationFloor;
// Change in inflation rate per round until the target bonding rate is achieved
uint256 public inflationChange;
// Target bonding rate
Expand Down Expand Up @@ -70,15 +74,24 @@ contract Minter is Manager, IMinter {
* @param _inflation Base inflation rate as a percentage of current total token supply
* @param _inflationChange Change in inflation rate each round (increase or decrease) if target bonding rate is not achieved
* @param _targetBondingRate Target bonding rate as a percentage of total bonded tokens / total token supply
* @param _inflationCeiling Inflation rate ceiling as a percentage of current total token supply
* @param _inflationFloor Inflation rate floor as a percentage of current total token supply
*/
constructor(
address _controller,
uint256 _inflation,
uint256 _inflationChange,
uint256 _targetBondingRate
uint256 _targetBondingRate,
uint256 _inflationCeiling,
uint256 _inflationFloor
) Manager(_controller) {
// Inflation must be valid percentage
require(MathUtils.validPerc(_inflation), "_inflation is invalid percentage");
// Inflation bounds must be valid percentages
require(MathUtils.validPerc(_inflationCeiling), "_inflationCeiling is invalid percentage");
require(MathUtils.validPerc(_inflationFloor), "_inflationFloor is invalid percentage");
// Inflation floor should be lower or equal to the ceiling
require(_inflationFloor <= _inflationCeiling, "_inflationFloor must be <= _inflationCeiling");
// Inflation change must be valid percentage
require(MathUtils.validPerc(_inflationChange), "_inflationChange is invalid percentage");
// Target bonding rate must be valid percentage
Expand All @@ -87,6 +100,8 @@ contract Minter is Manager, IMinter {
inflation = _inflation;
inflationChange = _inflationChange;
targetBondingRate = _targetBondingRate;
inflationCeiling = _inflationCeiling;
inflationFloor = _inflationFloor;
}

/**
Expand Down Expand Up @@ -115,6 +130,36 @@ contract Minter is Manager, IMinter {
emit ParameterUpdate("inflationChange");
}

/**
* @notice Set inflationCeiling. Only callable by Controller owner
* @param _inflationCeiling New inflation cap as a percentage of total token supply
*/
function setInflationCeiling(uint256 _inflationCeiling) external onlyControllerOwner {
// Must be valid percentage
require(MathUtils.validPerc(_inflationCeiling), "_inflationCeiling is invalid percentage");
// Inflation ceiling should be higher or equal to the floor
require(_inflationCeiling >= inflationFloor, "_inflationCeiling must be >= inflationFloor");

inflationCeiling = _inflationCeiling;

emit ParameterUpdate("inflationCeiling");
}

/**
* @notice Set inflationFloor. Only callable by Controller owner
* @param _inflationFloor New inflation floor as a percentage of total token supply
*/
function setInflationFloor(uint256 _inflationFloor) external onlyControllerOwner {
// Must be valid percentage
require(MathUtils.validPerc(_inflationFloor), "_inflationFloor is invalid percentage");
// Inflation floor should be lower or equal to the ceiling
require(_inflationFloor <= inflationCeiling, "_inflationFloor must be <= inflationCeiling");

inflationFloor = _inflationFloor;

emit ParameterUpdate("inflationFloor");
}

/**
* @notice Migrate to a new Minter by transferring the current Minter's LPT + ETH balance to the new Minter
* @dev Only callable by Controller owner
Expand All @@ -138,6 +183,21 @@ contract Minter is Manager, IMinter {
_newMinter.depositETH{ value: address(this).balance }();
}

/**
* @notice Migrate state variables affected by RoundsManager from the old Minter
* @dev Only callable by Controller owner
*/
function migrateOldMinterState() external onlyControllerOwner {
IMinter oldMinter = IMinter(controller.getContract(keccak256("Minter")));
// Old Minter cannot be the current Minter
require(address(oldMinter) != address(this), "old Minter cannot be current Minter");

// Transfer state from old Minter
currentMintableTokens = oldMinter.currentMintableTokens();
currentMintedTokens = oldMinter.currentMintedTokens();
inflation = oldMinter.inflation();
Comment on lines +195 to +198
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we copy all fields including inflationChange and targetBondingRate to make sure we get a consistent state snaphot? Otherwise, we'd be relying on the transaction that constructed the contract to have initialized it with the same values as the current minter.

We usually run the contract creation separately so that we have a fixed address for the governor transaction. In that case it probably makes sense to make the upgrade tx "self-contained", copying all state, not relying on args configured in other txs. WDYT?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two reasons why we thought it would rather be more confusing:

  • We wouldn't be able to copy all possible fields, since maxInflation and minInflation are not part of the currently deployed contract. In other words it would be confusing during the next upgrade that everything is copied except for those 2 values
  • The overwrite of other arguments is redundant and can create additional confusion. For example, in case governance decides to also change some of the configurable parameters (e.g. inflationChange) during the Minter upgrade, it will result in 1) deploying the contract with the new or some values 2) calling migrateOldMinterState that will reset them to the old state 3) calling set* functions to set them to the new values

Based on this, we've decided to update only the minimum required state that is dynamically changing.

Copy link
Member

@victorges victorges Apr 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that the contracts are not created on the governance transaction, so someone would need to have run a separate transaction first to deploy the new Minter contract. This transaction would not be on governor-scripts repo so it would need to be reviewed separately, creating further complexity on the deployment flow (or at least that's how I remember we did it before).

So IMO an extra transaction to change any other parameters that the governance wants would be preferred, as it would also be very explicit (and not set on a "deploy contract" transaction that is not necessarily audited on the governance process).

Regarding not copying the maxInflation and minInflation, I think it could be covered by a local comment saying that the fields were added with the migration function, so they should be copied if a new version is made. WDYT?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

creating further complexity on the deployment flow (or at least that's how I remember we did it before).

I'm not sure where the additional complexity is coming from, as the deployed contract and its state have to be reviewed anyway. Typically, the review of the contract should be part of the standard "governance payload review": if the payload mentions/interacts with a new contract, the code as well as the state have to be checked. In my opinion, having 2 places where a state variable is being modified is rather confusing than "explicit".

So IMO an extra transaction to change any other parameters that the governance wants would be preferred, as it would also be very explicit (and not set on a "deploy contract" transaction that is not necessarily audited on the governance process).

Same problem here: do you then would also suggest to set inflation ceiling and floor in the governance payload to make it explicit? (although those values have already been set at deployment via the constructor arguments)

Regarding not copying the maxInflation and minInflation, I think it could be covered by a local comment

If we still would go with updating migrateOldMinterState scope, what do you mean by the "local" comment? Some kind of TODO comment inside the function (e.g. "TODO: add inflationCeiling and inflationFloor once old miter has those values")?

}

/**
* @notice Create reward based on a fractional portion of the mintable tokens for the current round
* @param _fracNum Numerator of fraction (active transcoder's stake)
Expand Down Expand Up @@ -240,13 +300,22 @@ contract Minter is Manager, IMinter {
currentBondingRate = MathUtils.percPoints(totalBonded, totalSupply);
}

if (currentBondingRate < targetBondingRate) {
// Adjust inflation based on current bonding rate and target bonding rate, ensuring it stays within the floor and ceiling
if ((currentBondingRate < targetBondingRate && inflation < inflationCeiling) || inflation < inflationFloor) {
// Bonding rate is below the target - increase inflation
inflation = inflation.add(inflationChange);
} else if (currentBondingRate > targetBondingRate) {
if (inflation.add(inflationChange) > inflationCeiling) {
// If inflation would go above the ceiling, set it to the ceiling
inflation = inflationCeiling;
} else {
inflation = inflation.add(inflationChange);
}
} else if (
(currentBondingRate > targetBondingRate && inflation > inflationFloor) || inflation > inflationCeiling
) {
// Bonding rate is above the target - decrease inflation
if (inflationChange > inflation) {
inflation = 0;
if (inflationFloor.add(inflationChange) > inflation) {
// If inflation would go below the floor, set it to the floor
inflation = inflationFloor;
} else {
inflation = inflation.sub(inflationChange);
}
Expand Down
6 changes: 5 additions & 1 deletion deploy/deploy_contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ const func: DeployFunction = async function(hre: HardhatRuntimeEnvironment) {
Controller.address,
config.minter.inflation,
config.minter.inflationChange,
config.minter.targetBondingRate
config.minter.targetBondingRate,
config.minter.inflationCeiling,
config.minter.inflationFloor
]
})

Expand Down Expand Up @@ -372,6 +374,8 @@ const func: DeployFunction = async function(hre: HardhatRuntimeEnvironment) {

await (await Token.grantRole(MINTER_ROLE, minter.address)).wait()

await (await Token.grantRole(DEFAULT_ADMIN_ROLE, governor.address)).wait()

// Controller is owned by the deployer at this point
// transferOwnership() needs to be called separately to give ownership to the Governor
}
Expand Down
9 changes: 6 additions & 3 deletions deploy/deploy_minter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import {HardhatRuntimeEnvironment} from "hardhat/types"
import {DeployFunction} from "hardhat-deploy/types"
import {ethers} from "hardhat"
import {Minter} from "../typechain"
import getNetworkConfig from "./migrations.config"

const func: DeployFunction = async function(hre: HardhatRuntimeEnvironment) {
const {deployments, getNamedAccounts} = hre // Get the deployments and getNamedAccounts which are provided by hardhat-deploy
const {deploy} = deployments // the deployments object itself contains the deploy function
const config = getNetworkConfig(hre.network.name)

const {deployer} = await getNamedAccounts() // Fetch named accounts from hardhat.config.ts

Expand All @@ -18,16 +20,17 @@ const func: DeployFunction = async function(hre: HardhatRuntimeEnvironment) {
)) as Minter

const inflation = await minter.inflation()
const inflationChange = await minter.inflationChange()
const targetBondingRate = await minter.targetBondingRate()

await deploy("Minter", {
from: deployer,
args: [
controllerDeployment.address,
inflation,
inflationChange,
targetBondingRate
config.minter.inflationChange,
targetBondingRate,
config.minter.inflationCeiling,
config.minter.inflationFloor
]
})
}
Expand Down
29 changes: 17 additions & 12 deletions deploy/migrations.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ const gethDev = {
minter: {
inflation: 137,
inflationChange: 3,
targetBondingRate: 500000
targetBondingRate: 500000,
inflationCeiling: 200,
inflationFloor: 100
}
}

Expand Down Expand Up @@ -51,7 +53,9 @@ const defaultConfig = {
minter: {
inflation: 137,
inflationChange: 3,
targetBondingRate: 500000
targetBondingRate: 500000,
inflationCeiling: 200,
inflationFloor: 100
},
treasury: {
minDelay: 0 // 0s initial proposal execution delay
Expand Down Expand Up @@ -99,7 +103,9 @@ const rinkeby = {
minter: {
inflation: 137,
inflationChange: 3,
targetBondingRate: 0
targetBondingRate: 0,
inflationCeiling: 200,
inflationFloor: 100
}
}

Expand All @@ -126,7 +132,9 @@ const arbitrumRinkeby = {
minter: {
inflation: 137,
inflationChange: 3,
targetBondingRate: 0
targetBondingRate: 0,
inflationCeiling: 200,
inflationFloor: 100
}
}

Expand Down Expand Up @@ -154,14 +162,11 @@ const arbitrumMainnet = {
roundLockAmount: 100000
},
minter: {
// As of L1 round 2460 inflation was 221500 and bonding rate > 50% so inflation was declining
// The switch to L2 projected to occur in L1 round 2466
// If inflation continues to decrease inflation projected to be 221500 - (6 * 500) = 218500 in L1 round 2466
// No reward calls will happen on L2 until the round after migrations start since it takes a round for orchestrators to become active
// The inflation at the start of that round will be 218500 - 500 = 218000
inflation: 218500,
inflationChange: 500,
targetBondingRate: 500000000
inflation: 651000, // Current value at round 3766, but is overwritten by `migrateOldMinterState`
inflationChange: 1000, // Set according to LIP-100
targetBondingRate: 500000000,
inflationCeiling: 750000, // Set according to LIP-100
inflationFloor: 50000 // Set according to LIP-100
},
treasury: {
minDelay: 0 // 0s initial proposal execution delay
Expand Down
Loading