Skip to content

Commit

Permalink
Meta & Ecosystem Rewards (#90)
Browse files Browse the repository at this point in the history
* Implemented MTA token
* Implemented Rewards mechanisms
* Tested suite
  • Loading branch information
alsco77 committed Jul 30, 2020
1 parent d4180e0 commit 950abdd
Show file tree
Hide file tree
Showing 37 changed files with 4,703 additions and 41 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ package-lock.json
.coverage_artifacts/
.coverage_contracts/
coverage.json
cache

# sol-merger
flat/
Expand Down
1 change: 1 addition & 0 deletions .solcover.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = {
skipFiles: [
'Migrations.sol',
'interfaces',
'integrations',
'z_mocks',
'shared/InitializableReentrancyGuard.sol',
'integrations'
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<img src="https://mstable.org/assets/img/email/mstable_logo_horizontal_black.png" width="420" >

[![CircleCI](https://circleci.com/gh/mstable/mStable-contracts.svg?style=svg&circle-token=a8bb29a97a0a0949a15cc28bd9b2245960287bc2)](https://circleci.com/gh/mstable/mStable-contracts)
[![Coverage Status](https://coveralls.io/repos/github/mstable/mStable-contracts/badge.svg?t=7A5XxE)](https://coveralls.io/github/mstable/mStable-contracts)
[![Coverage Status](https://coveralls.io/repos/github/mstable/mStable-contracts/badge.svg?branch=master)](https://coveralls.io/github/mstable/mStable-contracts?branch=master)
[![Discord](https://img.shields.io/discord/525087739801239552?color=7289DA&label=discord%20)](https://discordapp.com/channels/525087739801239552/)


Expand Down
2 changes: 2 additions & 0 deletions contracts/governance/ClaimableGovernor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { Governable } from "./Governable.sol";
* @title ClaimableGovernor
* @author Stability Labs Pty. Ltd.
* @notice 2 way handshake for Governance transfer
* @dev Overrides the public functions in Governable to provide
* a second step of validation.
*/
contract ClaimableGovernor is Governable {

Expand Down
4 changes: 3 additions & 1 deletion contracts/governance/Governable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ pragma solidity 0.5.16;
* @title Governable
* @author Stability Labs Pty. Ltd.
* @notice Simple contract implementing an Ownable pattern.
* @dev Derives from OpenZeppelin 2.3.0 Ownable.sol
* @dev Derives from V2.3.0 @openzeppelin/contracts/ownership/Ownable.sol
* Modified to have custom name and features
* - Removed `renounceOwnership`
* - Changes `_owner` to `_governor`
*/
contract Governable {

Expand Down
2 changes: 2 additions & 0 deletions contracts/governance/InitializableGovernableWhitelist.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { InitializableModule } from "../shared/InitializableModule.sol";
* @author Stability Labs Pty. Ltd.
* @notice Contract to store whitelisted address. The onlyWhitelisted() modifier should be used
* to allow the function calls only from the whitelisted addresses.
* @dev Implementing contracts are responsible for adding their own add and remove methods
* as the original purpose of this whitelist is for read optimisation.
*/
contract InitializableGovernableWhitelist is InitializableModule {

Expand Down
25 changes: 25 additions & 0 deletions contracts/interfaces/IMetaToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
pragma solidity 0.5.16;

/**
* @title IMetaToken
* @dev Interface for MetaToken
*/
interface IMetaToken {
/** @dev Basic ERC20 funcs */
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

/** @dev Burnable */
function burn(uint256 amount) external;
function burnFrom(address account, uint256 amount) external;

/** @dev Mintable */
function mint(address account, uint256 amount) external returns (bool);
}
8 changes: 8 additions & 0 deletions contracts/interfaces/IRewardsDistributionRecipient.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
pragma solidity 0.5.16;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IRewardsDistributionRecipient {
function notifyRewardAmount(uint256 reward) external;
function getRewardToken() external view returns (IERC20);
}
56 changes: 56 additions & 0 deletions contracts/meta-token/GovernedMinterRole.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
pragma solidity 0.5.16;

import { Module } from "../shared/Module.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { Roles } from "@openzeppelin/contracts/access/Roles.sol";

/**
* @title GovernedMinterRole
* @author OpenZeppelin (forked from @openzeppelin/contracts/access/roles/MinterRole.sol)
* @dev Forked from OpenZeppelin 'MinterRole' with changes:
* - `addMinter` modified from `onlyMinter` to `onlyGovernor`
* - `removeMinter` function added, callable by `onlyGovernor`
*/
contract GovernedMinterRole is Module {

using Roles for Roles.Role;

event MinterAdded(address indexed account);
event MinterRemoved(address indexed account);

Roles.Role private _minters;

constructor(address _nexus) internal Module(_nexus) {
}

modifier onlyMinter() {
require(isMinter(msg.sender), "MinterRole: caller does not have the Minter role");
_;
}

function isMinter(address account) public view returns (bool) {
return _minters.has(account);
}

function addMinter(address account) public onlyGovernor {
_addMinter(account);
}

function removeMinter(address account) public onlyGovernor {
_removeMinter(account);
}

function renounceMinter() public {
_removeMinter(msg.sender);
}

function _addMinter(address account) internal {
_minters.add(account);
emit MinterAdded(account);
}

function _removeMinter(address account) internal {
_minters.remove(account);
emit MinterRemoved(account);
}
}
53 changes: 53 additions & 0 deletions contracts/meta-token/MetaToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
pragma solidity 0.5.16;

import { GovernedMinterRole } from "./GovernedMinterRole.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { ERC20Detailed } from "@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol";
import { ERC20Burnable } from "@openzeppelin/contracts/token/ERC20/ERC20Burnable.sol";

/**
* @title MetaToken
* @author Stability Labs Pty. Ltd.
* @dev MetaToken is an ERC20 token, with mint privileges governed by mStable
* governors
*/
contract MetaToken is
ERC20,
GovernedMinterRole,
ERC20Detailed,
ERC20Burnable
{

/**
* @dev MetaToken simply implements a detailed ERC20 token,
* and a governed list of minters
*/
constructor(
address _nexus,
address _initialRecipient
)
public
GovernedMinterRole(_nexus)
ERC20Detailed(
"Meta",
"MTA",
18
)
{
// 100m initial supply
_mint(_initialRecipient, 100000000 * (10 ** 18));
}

// Forked from @openzeppelin
/**
* @dev See {ERC20-_mint}.
*
* Requirements:
*
* - the caller must have the {MinterRole}.
*/
function mint(address account, uint256 amount) public onlyMinter returns (bool) {
_mint(account, amount);
return true;
}
}
28 changes: 28 additions & 0 deletions contracts/rewards/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@


## Files

Why so many?

Actually only 3 are base contracts

### RewardsDistributor

Allows reward allocators ("FundManagers") to distribute rewards.

### StakingRewards

`StakingRewards` is `RewardsDistributionRecipient`
--------------> is `StakingTokenWrapper`

This preserves the code written, tested, audited and deployed by `Synthetix` (StakingRewards & StakingTokenWrapper).

Originally: Synthetix (forked from /Synthetixio/synthetix/contracts/StakingRewards.sol)
Audit: https://github.com/sigp/public-audits/blob/master/synthetix/unipool/review.pdf`

### StakingRewardsWithPlatformToken

`StakingRewardsWithPlatformToken` is `RewardsDistributionRecipient`
-------------------------------> is `StakingTokenWrapper`

`StakingRewardsWithPlatformToken` deploys `PlatformTokenVendor` during its constructor
49 changes: 49 additions & 0 deletions contracts/rewards/RewardsDistributionRecipient.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
pragma solidity 0.5.16;

import { Module } from "../shared/Module.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IRewardsDistributionRecipient } from "../interfaces/IRewardsDistributionRecipient.sol";

/**
* @title RewardsDistributionRecipient
* @author Originally: Synthetix (forked from /Synthetixio/synthetix/contracts/RewardsDistributionRecipient.sol)
* Changes by: Stability Labs Pty. Ltd.
* @notice RewardsDistributionRecipient gets notified of additional rewards by the rewardsDistributor
* @dev Changes: Addition of Module and abstract `getRewardToken` func + cosmetic
*/
contract RewardsDistributionRecipient is IRewardsDistributionRecipient, Module {

// @abstract
function notifyRewardAmount(uint256 reward) external;
function getRewardToken() external view returns (IERC20);

// This address has the ability to distribute the rewards
address public rewardsDistributor;

/** @dev Recipient is a module, governed by mStable governance */
constructor(address _nexus, address _rewardsDistributor)
internal
Module(_nexus)
{
rewardsDistributor = _rewardsDistributor;
}

/**
* @dev Only the rewards distributor can notify about rewards
*/
modifier onlyRewardsDistributor() {
require(msg.sender == rewardsDistributor, "Caller is not reward distributor");
_;
}

/**
* @dev Change the rewardsDistributor - only called by mStable governor
* @param _rewardsDistributor Address of the new distributor
*/
function setRewardsDistribution(address _rewardsDistributor)
external
onlyGovernor
{
rewardsDistributor = _rewardsDistributor;
}
}
87 changes: 87 additions & 0 deletions contracts/rewards/RewardsDistributor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
pragma solidity 0.5.16;

import { IRewardsDistributionRecipient } from "../interfaces/IRewardsDistributionRecipient.sol";

import { InitializableGovernableWhitelist } from "../governance/InitializableGovernableWhitelist.sol";
import { SafeERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";

/**
* @title RewardsDistributor
* @author Stability Labs Pty. Ltd.
* @notice RewardsDistributor allows Fund Managers to send rewards (usually in MTA)
* to specified Reward Recipients.
*/
contract RewardsDistributor is InitializableGovernableWhitelist {

using SafeERC20 for IERC20;

event RemovedFundManager(address indexed _address);
event DistributedReward(address funder, address recipient, address rewardToken, uint256 amount);

/** @dev Recipient is a module, governed by mStable governance */
constructor(
address _nexus,
address[] memory _fundManagers
)
public
{
InitializableGovernableWhitelist._initialize(_nexus, _fundManagers);
}

/**
* @dev Allows the mStable governance to add a new FundManager
* @param _address FundManager to add
*/
function addFundManager(address _address)
external
onlyGovernor
{
_addWhitelist(_address);
}

/**
* @dev Allows the mStable governance to remove inactive FundManagers
* @param _address FundManager to remove
*/
function removeFundManager(address _address)
external
onlyGovernor
{
require(_address != address(0), "Address is zero");
require(whitelist[_address], "Address is not whitelisted");

whitelist[_address] = false;

emit RemovedFundManager(_address);
}

/**
* @dev Distributes reward tokens to list of recipients and notifies them
* of the transfer. Only callable by FundManagers
* @param _recipients Array of Reward recipients to credit
* @param _amounts Amounts of reward tokens to distribute
*/
function distributeRewards(
IRewardsDistributionRecipient[] calldata _recipients,
uint256[] calldata _amounts
)
external
onlyWhitelisted
{
uint256 len = _recipients.length;
require(len > 0, "Must choose recipients");
require(len == _amounts.length, "Mismatching inputs");

for(uint i = 0; i < len; i++){
uint256 amount = _amounts[i];
IRewardsDistributionRecipient recipient = _recipients[i];
// Send the RewardToken to recipient
IERC20 rewardToken = recipient.getRewardToken();
rewardToken.safeTransferFrom(msg.sender, address(recipient), amount);
// Only after successfull tx - notify the contract of the new funds
recipient.notifyRewardAmount(amount);

emit DistributedReward(msg.sender, address(recipient), address(rewardToken), amount);
}
}
}

0 comments on commit 950abdd

Please sign in to comment.