Skip to content
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

CREDIT token #204

Open
wants to merge 3 commits into
base: feat/redeemer
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
7 changes: 7 additions & 0 deletions src/core/VoltRoles.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ library VoltRoles {
/// @notice can mint VOLT arbitrarily
bytes32 internal constant MINTER = keccak256("MINTER_ROLE");

/// @notice can mint CREDIT arbitrarily
bytes32 internal constant CREDIT_MINTER = keccak256("CREDIT_MINTER_ROLE");

/// @notice can mint CREDIT within rate limits
bytes32 internal constant RATE_LIMITED_CREDIT_MINTER =
keccak256("RATE_LIMITED_CREDIT_MINTER_ROLE");

/// @notice can mint GUILD arbitrarily
bytes32 internal constant GUILD_MINTER = keccak256("GUILD_MINTER_ROLE");

Expand Down
26 changes: 26 additions & 0 deletions src/governance/CreditToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity =0.8.13;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol";
import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol";

/**
@title CREDIT ERC20 Token
@author eswak
@notice This is the debt token of the Ethereum Credit Guild.
*/
contract CreditToken is CoreRefV2, ERC20Burnable {
constructor(
address _core
) CoreRefV2(_core) ERC20("Ethereum Credit Guild - CREDIT", "CREDIT") {}

/// @notice mint new tokens to the target address
function mint(
address to,
uint256 amount
) external onlyVoltRole(VoltRoles.CREDIT_MINTER) {
_mint(to, amount);
}
}
7 changes: 7 additions & 0 deletions src/governance/ERC20Gauges.sol
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,13 @@ abstract contract ERC20Gauges is ERC20 {
address account,
bool canExceedMax
) internal {
if (canExceedMax) {
require(
account.code.length != 0,
"ERC20Gauges: not a smart contract"
);
}

canExceedMaxGauges[account] = canExceedMax;

emit CanExceedMaxGaugesUpdate(account, canExceedMax);
Expand Down
62 changes: 62 additions & 0 deletions src/rate-limits/RateLimitedCreditMinter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity =0.8.13;

import {CoreRefV2} from "@voltprotocol/refs/CoreRefV2.sol";
import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol";
import {CreditToken} from "@voltprotocol/governance/CreditToken.sol";
import {RateLimitedV2} from "@voltprotocol/utils/RateLimitedV2.sol";

/// @notice contract to mint CREDIT on a rate limit.
/// All minting should flow through this smart contract, as it should be the only one with
/// minting capabilities.
contract RateLimitedCreditMinter is RateLimitedV2 {
/// @notice the reference to CREDIT token
address public token;

/// @param _core reference to the core smart contract
/// @param _token reference to the token to mint
/// @param _maxRateLimitPerSecond maximum rate limit per second that governance can set
/// @param _rateLimitPerSecond starting rate limit per second for Volt minting
/// @param _bufferCap cap on buffer size for this rate limited instance
constructor(
address _core,
address _token,
uint256 _maxRateLimitPerSecond,
uint128 _rateLimitPerSecond,
uint128 _bufferCap
)
CoreRefV2(_core)
RateLimitedV2(_maxRateLimitPerSecond, _rateLimitPerSecond, _bufferCap)
{
token = _token;
}

/// @notice Mint new tokens.
/// Pausable and depletes the buffer, reverts if buffer is used.
/// @param to the recipient address of the minted tokens.
/// @param amount the amount of tokens to mint.
function mint(
address to,
uint256 amount
)
external
onlyVoltRole(VoltRoles.RATE_LIMITED_CREDIT_MINTER)
whenNotPaused
{
_depleteBuffer(amount); /// check and effects
CreditToken(token).mint(to, amount); /// interactions
}

/// @notice replenish the buffer.
/// This can be used when tokens are burnt, for instance.
/// @param amount of tokens to replenish buffer by
function replenishBuffer(
uint256 amount
)
external
onlyVoltRole(VoltRoles.RATE_LIMITED_CREDIT_MINTER)
whenNotPaused
{
_replenishBuffer(amount); /// effects
}
}
60 changes: 60 additions & 0 deletions test/unit/governance/CreditToken.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.13;

import {Test} from "@forge-std/Test.sol";
import {CoreV2} from "@voltprotocol/core/CoreV2.sol";
import {getCoreV2} from "@test/unit/utils/Fixtures.sol";
import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol";
import {CreditToken} from "@voltprotocol/governance/CreditToken.sol";
import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol";

contract CreditTokenUnitTest is Test {
CoreV2 private core;
CreditToken token;
address constant alice = address(0x616c696365);
address constant bob = address(0xB0B);

function setUp() public {
vm.warp(1679067867);
vm.roll(16848497);
core = CoreV2(address(getCoreV2()));
token = new CreditToken(address(core));

// labels
vm.label(address(core), "core");
vm.label(address(token), "token");
vm.label(alice, "alice");
vm.label(bob, "bob");
}

function testInitialState() public {
assertEq(address(token.core()), address(core));
assertEq(token.balanceOf(alice), 0);
assertEq(token.totalSupply(), 0);
}

function testMintAccessControl() public {
// without role, mint reverts
vm.expectRevert("UNAUTHORIZED");
token.mint(alice, 100);

// create/grant role
vm.startPrank(addresses.governorAddress);
core.createRole(VoltRoles.CREDIT_MINTER, VoltRoles.GOVERNOR);
core.grantRole(VoltRoles.CREDIT_MINTER, address(this));
vm.stopPrank();

// mint tokens for alice
token.mint(alice, 100);
assertEq(token.balanceOf(alice), 100);
assertEq(token.balanceOf(bob), 0);
assertEq(token.totalSupply(), 100);

// alice transfers to bob
vm.prank(alice);
token.transfer(bob, 100);
assertEq(token.balanceOf(alice), 0);
assertEq(token.balanceOf(bob), 100);
assertEq(token.totalSupply(), 100);
}
}
4 changes: 4 additions & 0 deletions test/unit/governance/ERC20Gauges.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ contract ERC20GaugesUnitTest is Test {
function testSetCanExceedMaxGauges() public {
token.setCanExceedMaxGauges(address(this), true);
require(token.canExceedMaxGauges(address(this)));

// revert for non-smart contracts
vm.expectRevert("ERC20Gauges: not a smart contract");
token.setCanExceedMaxGauges(address(0xBEEF), true);
}

function testAddGauge(address[8] memory gauges) public {
Expand Down
127 changes: 127 additions & 0 deletions test/unit/limiter/RateLimitedCreditMinter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.13;

import {Test} from "@forge-std/Test.sol";
import {CoreV2} from "@voltprotocol/core/CoreV2.sol";
import {MockERC20} from "@test/mock/MockERC20.sol";
import {getCoreV2} from "@test/unit/utils/Fixtures.sol";
import {VoltRoles} from "@voltprotocol/core/VoltRoles.sol";
import {RateLimitedCreditMinter} from "@voltprotocol/rate-limits/RateLimitedCreditMinter.sol";
import {TestAddresses as addresses} from "@test/unit/utils/TestAddresses.sol";

contract RateLimitedCreditMinterUnitTest is Test {
RateLimitedCreditMinter public rlcm;
MockERC20 private token;
CoreV2 private core;
address constant alice = address(0x616c696365);

uint256 MAX_RATE_LIMIT_PER_SECOND = 10 ether;
uint128 RATE_LIMIT_PER_SECOND = 10 ether;
uint128 BUFFER_CAP = 10_000_000 ether;

function setUp() public {
vm.warp(1679067867);
vm.roll(16848497);
core = getCoreV2();
token = new MockERC20();
rlcm = new RateLimitedCreditMinter(
address(core),
address(token),
MAX_RATE_LIMIT_PER_SECOND,
RATE_LIMIT_PER_SECOND,
BUFFER_CAP
);

vm.label(address(token), "token");
vm.label(address(core), "core");
vm.label(address(rlcm), "rlcm");
vm.label(address(this), "test");
}

function testInitialState() public {
assertEq(address(rlcm.core()), address(core));
assertEq(rlcm.token(), address(token));
}

function testMint() public {
// without role, minting reverts
vm.expectRevert("UNAUTHORIZED");
rlcm.mint(address(this), 100);

// create/grant role
vm.startPrank(addresses.governorAddress);
core.createRole(
VoltRoles.RATE_LIMITED_CREDIT_MINTER,
VoltRoles.GOVERNOR
);
core.grantRole(VoltRoles.RATE_LIMITED_CREDIT_MINTER, address(this));
vm.stopPrank();

// mint tokens for alice
rlcm.mint(alice, 100);
assertEq(token.balanceOf(alice), 100);
assertEq(rlcm.buffer(), BUFFER_CAP - 100);
}

function testReplenishBuffer() public {
// without role, replenishBuffer reverts
vm.expectRevert("UNAUTHORIZED");
rlcm.replenishBuffer(100);

// create/grant role
vm.startPrank(addresses.governorAddress);
core.createRole(
VoltRoles.RATE_LIMITED_CREDIT_MINTER,
VoltRoles.GOVERNOR
);
core.grantRole(VoltRoles.RATE_LIMITED_CREDIT_MINTER, address(this));
vm.stopPrank();

// mint all the available buffer for alice
rlcm.mint(alice, rlcm.buffer());
assertEq(token.balanceOf(alice), BUFFER_CAP);

// trying to mint more reverts
vm.expectRevert("RateLimited: no rate limit buffer");
rlcm.mint(alice, 100);

// replenish buffer
rlcm.replenishBuffer(100);
assertEq(rlcm.buffer(), 100);

// can mint the replenished amount
rlcm.mint(alice, 100);
}

function testMintPausable() public {
// create/grant role
vm.startPrank(addresses.governorAddress);
core.createRole(
VoltRoles.RATE_LIMITED_CREDIT_MINTER,
VoltRoles.GOVERNOR
);
core.grantRole(VoltRoles.RATE_LIMITED_CREDIT_MINTER, address(this));
rlcm.pause();
vm.stopPrank();

// minting reverts because the contract is paused
vm.expectRevert("Pausable: paused");
rlcm.mint(alice, 100);
}

function testReplenishBufferPausable() public {
// create/grant role
vm.startPrank(addresses.governorAddress);
core.createRole(
VoltRoles.RATE_LIMITED_CREDIT_MINTER,
VoltRoles.GOVERNOR
);
core.grantRole(VoltRoles.RATE_LIMITED_CREDIT_MINTER, address(this));
rlcm.pause();
vm.stopPrank();

// replenishBuffer reverts because the contract is paused
vm.expectRevert("Pausable: paused");
rlcm.replenishBuffer(100);
}
}