Skip to content

Commit

Permalink
feat(Protocol): add activateMinter
Browse files Browse the repository at this point in the history
  • Loading branch information
PierrickGT committed Nov 30, 2023
1 parent 18bfb3f commit ac0ca00
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 43 deletions.
66 changes: 50 additions & 16 deletions src/Protocol.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ contract Protocol is IProtocol, ContinuousIndexing, StatelessERC712 {
uint256 internal _totalPrincipalOfActiveOwedM;
uint256 internal _totalInactiveOwedM;

mapping(address minter => bool isActiveMinter) internal _activeMinter;
mapping(address minter => uint256 collateral) internal _collaterals;
mapping(address minter => uint256 owedM) internal _inactiveOwedM;
mapping(address minter => uint256 activeOwedM) internal _principalOfActiveOwedM;
Expand All @@ -67,18 +68,21 @@ contract Protocol is IProtocol, ContinuousIndexing, StatelessERC712 {
| Modifiers and Constructor |
\******************************************************************************************************************/

modifier onlyApprovedMinter() {
_revertIfNotApprovedMinter(msg.sender);
/// @notice Only allow active minter to call function.
modifier onlyActiveMinter() {
_revertIfInactiveMinter(msg.sender);

_;
}

/// @notice Only allow approved validator in SPOG to call function.
modifier onlyApprovedValidator() {
_revertIfNotApprovedValidator(msg.sender);

_;
}

/// @notice Only allow unfrozen minter to call function.
modifier onlyUnfrozenMinter() {
_revertIfMinterFrozen(msg.sender);

Expand All @@ -100,12 +104,22 @@ contract Protocol is IProtocol, ContinuousIndexing, StatelessERC712 {
| External Interactive Functions |
\******************************************************************************************************************/

/// @inheritdoc IProtocol
function activateMinter(address minter_) external {
if (!_isApprovedMinter(minter_)) revert NotApprovedMinter();
if (_activeMinter[minter_]) revert AlreadyActiveMinter();

_activeMinter[minter_] = true;

emit MinterActivated(minter_, msg.sender);
}

function burnM(address minter_, uint256 maxAmount_) external {
// NOTE: Penalize only for missed collateral updates, not for undercollateralization.
// Undercollateralization within one update interval is forgiven.
_imposePenaltyIfMissedCollateralUpdates(minter_);

uint256 amount_ = _isApprovedMinter(minter_) // TODO: `isActiveMinter`.
uint256 amount_ = _activeMinter[minter_]
? _repayForActiveMinter(minter_, maxAmount_)
: _repayForInactiveMinter(minter_, maxAmount_);

Expand All @@ -118,7 +132,7 @@ contract Protocol is IProtocol, ContinuousIndexing, StatelessERC712 {
updateIndex();
}

function cancelMint(uint256 mintId_) external onlyApprovedMinter {
function cancelMint(uint256 mintId_) external onlyActiveMinter {
_cancelMint(msg.sender, mintId_);
}

Expand All @@ -128,21 +142,22 @@ contract Protocol is IProtocol, ContinuousIndexing, StatelessERC712 {

function deactivateMinter(address minter_) external returns (uint256 inactiveOwedM_) {
if (_isApprovedMinter(minter_)) revert StillApprovedMinter();
_revertIfInactiveMinter(minter_);

// NOTE: Instead of imposing, calculate penalty and add it to `_inactiveOwedM` to save gas.
// TODO: And for undercollateralization?
inactiveOwedM_ = activeOwedMOf(minter_) + getPenaltyForMissedCollateralUpdates(minter_);

emit MinterDeactivated(minter_, inactiveOwedM_);
emit MinterDeactivated(minter_, inactiveOwedM_, msg.sender);

// TODO: Do not allow setting `_inactiveOwedM` to 0 by calling this function multiple times.
_inactiveOwedM[minter_] += inactiveOwedM_;
_totalInactiveOwedM += inactiveOwedM_;

// Adjust total principal of owed M.
_totalPrincipalOfActiveOwedM -= _principalOfActiveOwedM[minter_];

// Reset reasonable aspects of minter's state.
delete _activeMinter[minter_];
delete _collaterals[minter_];
delete _lastUpdateIntervals[minter_];
delete _lastCollateralUpdates[minter_];
Expand All @@ -157,12 +172,14 @@ contract Protocol is IProtocol, ContinuousIndexing, StatelessERC712 {
}

function freezeMinter(address minter_) external onlyApprovedValidator returns (uint256 frozenUntil_) {
_revertIfInactiveMinter(minter_);

frozenUntil_ = block.timestamp + SPOGRegistrarReader.getMinterFreezeTime(spogRegistrar);

emit MinterFrozen(minter_, _unfrozenTimestamps[minter_] = frozenUntil_);
}

function mintM(uint256 mintId_) external onlyApprovedMinter onlyUnfrozenMinter {
function mintM(uint256 mintId_) external onlyActiveMinter onlyUnfrozenMinter {
MintProposal storage mintProposal_ = _mintProposals[msg.sender];

(uint256 id_, uint256 amount_, uint256 createdAt_, address destination_) = (
Expand Down Expand Up @@ -202,7 +219,7 @@ contract Protocol is IProtocol, ContinuousIndexing, StatelessERC712 {
function proposeMint(
uint256 amount_,
address destination_
) external onlyApprovedMinter onlyUnfrozenMinter returns (uint256 mintId_) {
) external onlyActiveMinter onlyUnfrozenMinter returns (uint256 mintId_) {
_revertIfUndercollateralized(msg.sender, amount_); // Check that minter will remain sufficiently collateralized.

unchecked {
Expand All @@ -216,7 +233,7 @@ contract Protocol is IProtocol, ContinuousIndexing, StatelessERC712 {
emit MintProposed(mintId_, msg.sender, amount_, destination_);
}

function proposeRetrieval(uint256 collateral_) external onlyApprovedMinter returns (uint256 retrievalId_) {
function proposeRetrieval(uint256 collateral_) external onlyActiveMinter returns (uint256 retrievalId_) {
unchecked {
_retrievalNonce++;
}
Expand All @@ -238,7 +255,7 @@ contract Protocol is IProtocol, ContinuousIndexing, StatelessERC712 {
address[] calldata validators_,
uint256[] calldata timestamps_,
bytes[] calldata signatures_
) external onlyApprovedMinter returns (uint256 minTimestamp_) {
) external onlyActiveMinter returns (uint256 minTimestamp_) {
if (validators_.length != signatures_.length || signatures_.length != timestamps_.length) {
revert SignatureArrayLengthsMismatch();
}
Expand Down Expand Up @@ -292,7 +309,7 @@ contract Protocol is IProtocol, ContinuousIndexing, StatelessERC712 {

function activeOwedMOf(address minter_) public view returns (uint256 activeOwedM_) {
// TODO: This should also include the present value of unavoidable penalities. But then it would be very, if not
// impossible, to determine the `totalActiveOwedM` to them same standards. Perhaps we need a `penaltiesOf`
// impossible, to determine the `totalActiveOwedM` to the same standards. Perhaps we need a `penaltiesOf`
// external function to provide the present value of unavoidable penalities
return _getPresentValue(_principalOfActiveOwedM[minter_]);
}
Expand All @@ -319,6 +336,11 @@ contract Protocol is IProtocol, ContinuousIndexing, StatelessERC712 {
return _inactiveOwedM[minter_];
}

/// @inheritdoc IProtocol
function isActiveMinter(address minter_) external view returns (bool) {
return _activeMinter[minter_];
}

function latestMinterRate() external view returns (uint256 latestMinterRate_) {
return _latestRate;
}
Expand Down Expand Up @@ -521,10 +543,14 @@ contract Protocol is IProtocol, ContinuousIndexing, StatelessERC712 {
);
}

function _isApprovedMinter(address minter_) internal view returns (bool isApproved_) {
function _isApprovedMinter(address minter_) internal view returns (bool) {
return SPOGRegistrarReader.isApprovedMinter(spogRegistrar, minter_);
}

function _isApprovedValidator(address validator_) internal view returns (bool) {
return SPOGRegistrarReader.isApprovedValidator(spogRegistrar, validator_);
}

function _max(uint256 a_, uint256 b_) internal pure returns (uint256 max_) {
return a_ > b_ ? a_ : b_;
}
Expand All @@ -545,12 +571,20 @@ contract Protocol is IProtocol, ContinuousIndexing, StatelessERC712 {
if (block.timestamp < _unfrozenTimestamps[minter_]) revert FrozenMinter();
}

function _revertIfNotApprovedMinter(address minter_) internal view {
if (!_isApprovedMinter(minter_)) revert NotApprovedMinter();
/**
* @notice Reverts if minter is inactive.
* @param minter_ The address of the minter
*/
function _revertIfInactiveMinter(address minter_) internal view {
if (!_activeMinter[minter_]) revert InactiveMinter();
}

/**
* @notice Reverts if validator is not approved.
* @param validator_ The address of the validator
*/
function _revertIfNotApprovedValidator(address validator_) internal view {
if (!SPOGRegistrarReader.isApprovedValidator(spogRegistrar, validator_)) revert NotApprovedValidator();
if (!_isApprovedValidator(validator_)) revert NotApprovedValidator();
}

function _revertIfUndercollateralized(address minter_, uint256 additionalOwedM_) internal view {
Expand Down Expand Up @@ -602,7 +636,7 @@ contract Protocol is IProtocol, ContinuousIndexing, StatelessERC712 {
);

// Check that validator is approved by SPOG.
if (!SPOGRegistrarReader.isApprovedValidator(spogRegistrar, validators_[index_])) continue;
if (!_isApprovedValidator(validators_[index_])) continue;

// Check that ECDSA or ERC1271 signatures for given digest are valid.
if (!SignatureChecker.isValidSignature(validators_[index_], digest_, signatures_[index_])) continue;
Expand Down
61 changes: 55 additions & 6 deletions src/interfaces/IProtocol.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ interface IProtocol is IContinuousIndexing {
| Errors |
\******************************************************************************************************************/

/// @notice Emitted when calling `activeMinter` with an already active minter.
error AlreadyActiveMinter();

error ExpiredMintProposal();

error FrozenMinter();
Expand All @@ -19,6 +22,9 @@ interface IProtocol is IContinuousIndexing {

error InvalidSignatureOrder();

/// @notice Emitted when calling `deactivateMinter` with an inactive minter.
error InactiveMinter();

error NotApprovedMinter();

error NotApprovedValidator();
Expand All @@ -31,6 +37,7 @@ interface IProtocol is IContinuousIndexing {

error StaleCollateralUpdate();

/// @notice Emitted when calling `deactivateMinter` with a minter still approved in SPOG Registrar.
error StillApprovedMinter();

error Undercollateralized();
Expand All @@ -57,7 +64,20 @@ interface IProtocol is IContinuousIndexing {

event MintCanceled(uint256 indexed mintId, address indexed canceller);

event MinterDeactivated(address indexed minter, uint256 owedM);
/**
* @notice Emitted when a minter is activated.
* @param minter Address of the minter that was activated
* @param caller Address who called the function
*/
event MinterActivated(address indexed minter, address indexed caller);

/**
* @notice Emitted when a minter is deactivated.
* @param minter Address of the minter that was deactivated
* @param owedM Amount of M tokens owed by the minter
* @param caller Address who called the function
*/
event MinterDeactivated(address indexed minter, uint256 owedM, address indexed caller);

event MinterFrozen(address indexed minter, uint256 frozenUntil);

Expand All @@ -73,6 +93,14 @@ interface IProtocol is IContinuousIndexing {
| External Interactive Functions |
\******************************************************************************************************************/

/**
* @notice Activate an approved minter.
* @dev MUST revert if `minter` is not recorded as an approved minter in SPOG Registrar.
* @dev SHOULD revert if the minter is already active.
* @param minter The address of the minter to activate
*/
function activateMinter(address minter) external;

/**
* @notice Burns M tokens
* @param minter The address of the minter to burn M tokens for
Expand All @@ -83,6 +111,8 @@ interface IProtocol is IContinuousIndexing {

/**
* @notice Cancels minting request for minter
* @dev MUST only be callable by an active minter
* @dev An active minter that is not approved by SPOG Registrar anymore can still call cancelMint
* @param mintId The id of outstanding mint request
*/
function cancelMint(uint256 mintId) external;
Expand All @@ -94,11 +124,18 @@ interface IProtocol is IContinuousIndexing {
*/
function cancelMint(address minter, uint256 mintId) external;

/**
* @notice Deactivates an active minter.
* @dev MUST revert if the minter is not an approved minter.
* @dev SHOULD revert if the minter is not active.
* @param minter The address of the minter to deactivate
* @return inactiveOwedM The inactive owed M for the deactivated minter
*/
function deactivateMinter(address minter) external returns (uint256 inactiveOwedM);

/**
* @notice Freezes minter
* @param minter The address of the minter to freezeMinter
* @param minter The address of the minter to freeze
*/
function freezeMinter(address minter) external returns (uint256 frozenUntil_);

Expand Down Expand Up @@ -145,22 +182,34 @@ interface IProtocol is IContinuousIndexing {
/// @notice The EIP-712 typehash for the `updateCollateral` method.
function UPDATE_COLLATERAL_TYPEHASH() external pure returns (bytes32 typehash);

/// @notice The active owed M for a given active minter
function activeOwedMOf(address minter) external view returns (uint256 activeOwedM_);
/**
* @notice The active owed M for a given active minter.
* @param minter Address of the minter to get active owed M for
* @return activeOwedM The active owed M for the given active minter
*/
function activeOwedMOf(address minter) external view returns (uint256 activeOwedM);

/// @notice The collateral of a given minter.
function collateralOf(address minter) external view returns (uint256 collateral);

function collateralUpdateDeadlineOf(address minter) external view returns (uint256 lastUpdate);

/**
* @notice Returns the penalty for expired collateral value
* @param minter The address of the minter to get penalty for
* @notice Returns the penalty for expired collateral value.
* @dev Minter is penalized on current outstanding value per every missed interval.
* @dev Penalized only once per missed interval.
* @param minter Address of the minter to get penalty for
* @return The penalty for the given minter
*/
function getPenaltyForMissedCollateralUpdates(address minter) external view returns (uint256);

/**
* @notice Returns whether the given minter is active or not.
* @param minter Address of the minter to check
* @return True for an active minter, false otherwise
*/
function isActiveMinter(address minter) external view returns (bool);

/// @notice The inactive owed M for a given active minter
function inactiveOwedMOf(address minter) external view returns (uint256 inactiveOwedM);

Expand Down
14 changes: 11 additions & 3 deletions test/Integrations.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@ contract IntegrationTests is Test {
assertEq(_mToken.currentIndex(), 1_000034247161763120);
assertEq(_mToken.latestUpdateTimestamp(), latestMTokenUpdateTimestamp_);

vm.prank(_minters[0]);
vm.startPrank(_minters[0]);

_protocol.activateMinter(_minters[0]);
_protocol.updateCollateral(collateral, retrievalIds, bytes32(0), validators, timestamps, signatures);

// Both timestamps are updated since updateIndex gets called on the protocol, and thus on the mToken.
Expand All @@ -159,6 +161,8 @@ contract IntegrationTests is Test {
assertEq(_mToken.currentIndex(), 1_000034247161763120);
assertEq(_mToken.latestUpdateTimestamp(), latestMTokenUpdateTimestamp_);

vm.stopPrank();

vm.warp(block.timestamp + 1 hours); // 1 hour later, minter proposes a mint.

vm.prank(_alice);
Expand All @@ -177,7 +181,10 @@ contract IntegrationTests is Test {
assertEq(_mToken.currentIndex(), 1_000045663142986194);
assertEq(_mToken.latestUpdateTimestamp(), latestMTokenUpdateTimestamp_);

vm.prank(_minters[0]);
vm.startPrank(_minters[0]);

assertEq(_protocol.isActiveMinter(_minters[0]), true);

uint256 mintId = _protocol.proposeMint(mintAmount, _alice);

assertEq(_protocol.minterRate(), 1_000);
Expand Down Expand Up @@ -205,7 +212,6 @@ contract IntegrationTests is Test {
assertEq(_mToken.currentIndex(), 1_000194082758562663);
assertEq(_mToken.latestUpdateTimestamp(), latestMTokenUpdateTimestamp_);

vm.prank(_minters[0]);
_protocol.mintM(mintId);

// Both timestamps are updated since updateIndex gets called on the protocol, and thus on the mToken.
Expand Down Expand Up @@ -240,6 +246,8 @@ contract IntegrationTests is Test {
assertEq(_protocol.activeOwedMOf(_minters[0]), 551_224_560629); // ~500k with 10% APY compounded continuously.
assertEq(_mToken.balanceOf(_alice), 551_224_560629); // ~500k with 10% APY compounded continuously.

vm.stopPrank();

uint256 transferAmount_ = _mToken.balanceOf(_alice);

vm.prank(_alice);
Expand Down
Loading

0 comments on commit ac0ca00

Please sign in to comment.