Skip to content

Commit

Permalink
feat: make Token pausable (#366)
Browse files Browse the repository at this point in the history
Based on recent token-related conversations we've decided we want more
flexibility when it comes to the time between when we launch governance
and give utility to the token, and the time when the token is
transferable, i.e. tradable.
Most recent token launches have followed this approach, most notably
Safe.
These changes make the token ownable and pausable. 

However, even when paused the token can still be transferred by:
- The owner
- The locking contract - it can be locked and unlocked, thus the airgrab
can run while paused
- The emissions contract - it can be minted into existence while paused
  • Loading branch information
bowd authored Feb 13, 2024
1 parent 3b59273 commit 9ff0cfc
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 18 deletions.
10 changes: 9 additions & 1 deletion contracts/governance/GovernanceFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ contract GovernanceFactory is Ownable {
// ===========================================
// ========== Deploy 3: MentoToken ===========
// ===========================================

uint256 numberOfRecipients = allocationParams.additionalAllocationRecipients.length + 2;
address[] memory allocationRecipients = new address[](numberOfRecipients);
uint256[] memory allocationAmounts = new uint256[](numberOfRecipients);
Expand All @@ -160,7 +161,13 @@ contract GovernanceFactory is Ownable {
allocationAmounts[i + 2] = allocationParams.additionalAllocationAmounts[i];
}

mentoToken = MentoTokenDeployerLib.deploy(allocationRecipients, allocationAmounts, address(emission)); // NONCE:4
mentoToken = MentoTokenDeployerLib.deploy( // NONCE:4
allocationRecipients,
allocationAmounts,
address(emission),
lockingPrecalculated
);

assert(address(mentoToken) == tokenPrecalculated);

// ========================================
Expand Down Expand Up @@ -253,6 +260,7 @@ contract GovernanceFactory is Ownable {
emission.transferOwnership(address(governanceTimelock));
locking.transferOwnership(address(governanceTimelock));
proxyAdmin.transferOwnership(address(governanceTimelock));
mentoToken.transferOwnership(address(governanceTimelock));

emit GovernanceCreated(
address(proxyAdmin),
Expand Down
60 changes: 54 additions & 6 deletions contracts/governance/MentoToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@
pragma solidity 0.8.18;

import { ERC20Burnable, ERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import { Ownable } from "openzeppelin-contracts-next/contracts/access/Ownable.sol";
import { Pausable } from "openzeppelin-contracts-next/contracts/security/Pausable.sol";

/**
* @title Mento Token
* @author Mento Labs
* @notice This contract represents the Mento Protocol Token which is a Burnable ERC20 token.
*/
contract MentoToken is ERC20Burnable {
/// @notice The address of the emission contract that has the capability to emit new tokens.
contract MentoToken is Ownable, Pausable, ERC20Burnable {
/// @notice The address of the locking contract that has the capability to transfer tokens
/// even when the contract is paused.
address public immutable locking;

/// @notice The address of the emission contract that has the capability to emit new tokens
/// and transfer even when the contract is paused.
address public immutable emission;

/// @notice The total amount of tokens that can be minted by the emission contract.
Expand All @@ -25,19 +32,25 @@ contract MentoToken is ERC20Burnable {
* @param allocationRecipients_ The addresses of the initial token recipients.
* @param allocationAmounts_ The percentage of tokens to be allocated to each recipient.
* @param emission_ The address of the emission contract where the rest of the supply will be emitted.
* @param locking_ The address of the locking contract where the tokens will be locked.
*/
// solhint-enable max-line-length
constructor(
address[] memory allocationRecipients_,
uint256[] memory allocationAmounts_,
address emission_
) ERC20("Mento Token", "MENTO") {
address emission_,
address locking_
) ERC20("Mento Token", "MENTO") Ownable() {
require(emission_ != address(0), "MentoToken: emission is zero address");
require(locking_ != address(0), "MentoToken: locking is zero address");
require(
allocationRecipients_.length == allocationAmounts_.length,
"MentoToken: recipients and amounts length mismatch"
);

locking = locking_;
emission = emission_;

uint256 supply = 1_000_000_000 * 10**decimals();

uint256 totalAllocated;
Expand All @@ -50,9 +63,19 @@ contract MentoToken is ERC20Burnable {
_mint(allocationRecipients_[i], (supply * allocationAmounts_[i]) / 1000);
}
require(totalAllocated <= 1000, "MentoToken: total allocation exceeds 100%");

emission = emission_;
emissionSupply = (supply * (1000 - totalAllocated)) / 1000;

_pause();
}

/**
* @notice Unpauses all token transfers.
* @dev See {Pausable-_unpause}
* Requirements: caller must be the owner
*/
function unpause() public virtual onlyOwner {
require(paused(), "MentoToken: token is not paused");
_unpause();
}

/**
Expand All @@ -69,4 +92,29 @@ contract MentoToken is ERC20Burnable {
emittedAmount += amount;
_mint(target, amount);
}

/*
* @dev See {ERC20-_beforeTokenTransfer}
* Requirements: the contract must not be paused OR transfer must be initiated by owner
* @param from The account that is sending the tokens
* @param to The account that should receive the tokens
* @param amount Amount of tokens that should be transferred
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual override {
super._beforeTokenTransfer(from, to, amount);

require(to != address(this), "MentoToken: cannot transfer tokens to token contract");
// Token transfers are only possible if the contract is not paused
// OR if triggered by the owner of the contract
// OR if triggered by the locking contract
// OR if triggered by the emission contract
require(
!paused() || owner() == _msgSender() || locking == _msgSender() || emission == _msgSender(),
"MentoToken: token transfer while paused"
);
}
}
6 changes: 4 additions & 2 deletions contracts/governance/deployers/MentoTokenDeployerLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ library MentoTokenDeployerLib {
* @param allocationRecipients The addresses of the initial token recipients
* @param allocationAmounts The percentage of tokens to be allocated to each recipient
* @param emission The address of the emission contract
* @param locking The address of the locking contract
* @return The address of the new MentoToken contract
*/
function deploy(
address[] memory allocationRecipients,
uint256[] memory allocationAmounts,
address emission
address emission,
address locking
) external returns (MentoToken) {
return new MentoToken(allocationRecipients, allocationAmounts, emission);
return new MentoToken(allocationRecipients, allocationAmounts, emission, locking);
}
}
138 changes: 129 additions & 9 deletions test/governance/MentoToken.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,66 @@ import { MentoToken } from "contracts/governance/MentoToken.sol";
import { Arrays } from "test/utils/Arrays.sol";

contract MentoTokenTest is TestSetup {
event Paused(address account);

MentoToken public mentoToken;

address public mentoLabsMultiSig = makeAddr("mentoLabsMultiSig");
address public mentoLabsTreasuryTimelock = makeAddr("mentoLabsTreasuryTimelock");
address public airgrab = makeAddr("airgrab");
address public governanceTimelock = makeAddr("governanceTimelock");
address public emission = makeAddr("emission");
address public locking = makeAddr("locking");

uint256[] public allocationAmounts = Arrays.uints(80, 120, 50, 100);
address[] public allocationRecipients =
Arrays.addresses(mentoLabsMultiSig, mentoLabsTreasuryTimelock, airgrab, governanceTimelock);

modifier notPaused() {
mentoToken.unpause();
_;
}

function setUp() public {
mentoToken = new MentoToken(allocationRecipients, allocationAmounts, emission);
mentoToken = new MentoToken(allocationRecipients, allocationAmounts, emission, locking);
}

function test_constructor_whenEmissionIsZero_shouldRevert() public {
vm.expectRevert("MentoToken: emission is zero address");
mentoToken = new MentoToken(allocationRecipients, allocationAmounts, address(0));
mentoToken = new MentoToken(allocationRecipients, allocationAmounts, address(0), locking);
}

function test_constructor_whenLockingIsZero_shouldRevert() public {
vm.expectRevert("MentoToken: locking is zero address");
mentoToken = new MentoToken(allocationRecipients, allocationAmounts, emission, address(0));
}

function test_constructor_whenAllocationRecipientsAndAmountsLengthMismatch_shouldRevert() public {
vm.expectRevert("MentoToken: recipients and amounts length mismatch");
mentoToken = new MentoToken(allocationRecipients, Arrays.uints(80, 120, 50), emission);
mentoToken = new MentoToken(allocationRecipients, Arrays.uints(80, 120, 50), emission, locking);
}

function test_constructor_whenAllocationRecipientIsZero_shouldRevert() public {
vm.expectRevert("MentoToken: allocation recipient is zero address");
mentoToken = new MentoToken(
Arrays.addresses(mentoLabsMultiSig, mentoLabsTreasuryTimelock, airgrab, address(0)),
allocationAmounts,
emission
emission,
locking
);
}

function test_constructor_whenTotalAllocationExceeds1000_shouldRevert() public {
vm.expectRevert("MentoToken: total allocation exceeds 100%");
mentoToken = new MentoToken(allocationRecipients, Arrays.uints(80, 120, 50, 1000), emission);
mentoToken = new MentoToken(allocationRecipients, Arrays.uints(80, 120, 50, 1000), emission, locking);
}

function test_constructor_shouldPauseTheContract() public {
vm.expectEmit(true, true, true, true);
emit Paused(address(this));
mentoToken = new MentoToken(allocationRecipients, Arrays.uints(80, 120, 50, 100), emission, locking);

assertEq(mentoToken.paused(), true);
}

/// @dev Test the state initialization post-construction of the MentoToken contract.
Expand Down Expand Up @@ -79,7 +102,7 @@ contract MentoTokenTest is TestSetup {
* @notice Even though the burn function comes from OpenZeppelin's library,
* @notice this test assures correct integration.
*/
function test_burn_shouldBurnTokens() public {
function test_burn_shouldBurnTokens() public notPaused {
uint256 initialBalance = 3e18;
uint256 burnAmount = 1e18;
deal(address(mentoToken), alice, initialBalance);
Expand All @@ -98,7 +121,7 @@ contract MentoTokenTest is TestSetup {
* @notice Even though the burnFrom function comes from OpenZeppelin's library,
* @notice this test assures correct integration.
*/
function test_burnFrom_whenAllowed_shouldBurnTokens() public {
function test_burnFrom_whenAllowed_shouldBurnTokens() public notPaused {
uint256 initialBalance = 3e18;
uint256 burnAmount = 1e18;
deal(address(mentoToken), alice, initialBalance);
Expand Down Expand Up @@ -139,7 +162,7 @@ contract MentoTokenTest is TestSetup {
* @notice This test ensures that when the mint function is called with an amount that
* exceeds the total emission supply, the transaction should be reverted.
*/
function test_mint_whenAmountBiggerThanEmissionSupply_shouldRevert() public {
function test_mint_whenAmountBiggerThanEmissionSupply_shouldRevert() public notPaused {
uint256 mintAmount = 10e18;

vm.startPrank(emission);
Expand All @@ -160,7 +183,7 @@ contract MentoTokenTest is TestSetup {
* 2. The emittedAmount state variable correctly reflects the total amount of tokens emitted.
* 3. It can mint up to emission supply
*/
function test_mint_whenEmissionSupplyNotExceeded_shouldEmitTokens() public {
function test_mint_whenEmissionSupplyNotExceeded_shouldEmitTokens() public notPaused {
uint256 mintAmount = 10e18;

vm.startPrank(emission);
Expand All @@ -177,4 +200,101 @@ contract MentoTokenTest is TestSetup {
mentoToken.mint(alice, EMISSION_SUPPLY - 2 * mintAmount);
assertEq(mentoToken.emittedAmount(), EMISSION_SUPPLY);
}

function test_transfer_whenPaused_shouldRevert() public {
uint256 amount = 10e18;
deal(address(mentoToken), alice, amount);

vm.startPrank(alice);
vm.expectRevert("MentoToken: token transfer while paused");
mentoToken.transfer(bob, amount);
}

function test_transferFrom_whenPaused_shouldRevert() public {
uint256 amount = 10e18;
deal(address(mentoToken), alice, amount);
vm.prank(alice);
mentoToken.approve(bob, amount);

vm.startPrank(bob);
vm.expectRevert("MentoToken: token transfer while paused");
mentoToken.transferFrom(alice, bob, amount);
}

function test_transfer_whenPaused_calledByOwner_shouldWork() public {
uint256 amount = 10e18;
deal(address(mentoToken), address(this), amount);
mentoToken.transfer(bob, amount);
assertEq(mentoToken.balanceOf(bob), amount);
}

function test_transferFrom_whenPaused_calledByOwner_shouldWork() public {
uint256 amount = 10e18;
deal(address(mentoToken), alice, amount);
vm.prank(alice);
mentoToken.approve(address(this), amount);

mentoToken.transferFrom(alice, bob, amount);
assertEq(mentoToken.balanceOf(bob), amount);
}

function test_transfer_whenPaused_calledByLocking_shouldWork() public {
uint256 amount = 10e18;
deal(address(mentoToken), locking, amount);
vm.prank(locking);
mentoToken.transfer(bob, amount);
assertEq(mentoToken.balanceOf(bob), amount);
}

function test_transferFrom_whenPaused_calledByLocking_shouldWork() public {
uint256 amount = 10e18;
deal(address(mentoToken), alice, amount);
vm.prank(alice);
mentoToken.approve(locking, amount);

vm.prank(locking);
mentoToken.transferFrom(alice, bob, amount);
assertEq(mentoToken.balanceOf(bob), amount);
}

function test_transfer_whenPaused_calledByEmission_shouldWork() public {
uint256 amount = 10e18;
deal(address(mentoToken), emission, amount);
vm.prank(emission);
mentoToken.transfer(bob, amount);
assertEq(mentoToken.balanceOf(bob), amount);
}

function test_transferFrom_whenPaused_calledByEmission_shouldWork() public {
uint256 amount = 10e18;
deal(address(mentoToken), alice, amount);
vm.prank(alice);
mentoToken.approve(emission, amount);

vm.prank(emission);
mentoToken.transferFrom(alice, bob, amount);
assertEq(mentoToken.balanceOf(bob), amount);
}

function test_mint_whenPaused_calledByEmission_shouldWork() public {
vm.prank(emission);
mentoToken.mint(emission, 10e18);
assertEq(mentoToken.balanceOf(emission), 10e18);
}

function test_unpause_whenPaused_calledByOwner_shouldUnpause() public {
mentoToken.unpause();
assertEq(mentoToken.paused(), false);
}

function test_unpause_whenNotPaused_shouldRevert() public notPaused {
vm.expectRevert("MentoToken: token is not paused");
mentoToken.unpause();
}

function test_unpause_whenNotCalledByOwner_shouldRevert() public {
vm.prank(bob);
vm.expectRevert("Ownable: caller is not the owner");
mentoToken.unpause();
}
}

0 comments on commit 9ff0cfc

Please sign in to comment.