diff --git a/contracts/token/ERC20/RBACMintableToken.sol b/contracts/token/ERC20/RBACMintableToken.sol new file mode 100644 index 00000000000..f89931241df --- /dev/null +++ b/contracts/token/ERC20/RBACMintableToken.sol @@ -0,0 +1,41 @@ +pragma solidity ^0.4.21; + +import "./MintableToken.sol"; +import "../../ownership/rbac/RBACWithAdmin.sol"; + + +/** + * @title RBACMintable token + * @author Vittorio Minacori (@vittominacori) + * @dev Simple ERC20 Mintable Token, with RBAC minter permissions + */ +contract RBACMintableToken is MintableToken, RBACWithAdmin { + /** + * A constant role name for indicating minters. + */ + string public constant ROLE_MINTER = "minter"; + + /** + * @dev modifier to scope access to minters + * // reverts + */ + modifier onlyMinter() + { + checkRole(msg.sender, ROLE_MINTER); + _; + } + + /** + * @dev Function to mint tokens + * @param _to The address that will receive the minted tokens. + * @param _amount The amount of tokens to mint. + * @return A boolean that indicates if the operation was successful. + */ + function mint(address _to, uint256 _amount) onlyMinter canMint public returns (bool) { + totalSupply_ = totalSupply_.add(_amount); + balances[_to] = balances[_to].add(_amount); + emit Mint(_to, _amount); + emit Transfer(address(0), _to, _amount); + return true; + } +} diff --git a/test/crowdsale/RBACMintedCrowdsale.test.js b/test/crowdsale/RBACMintedCrowdsale.test.js new file mode 100644 index 00000000000..e0fe17fcd90 --- /dev/null +++ b/test/crowdsale/RBACMintedCrowdsale.test.js @@ -0,0 +1,63 @@ +import ether from '../helpers/ether'; + +const BigNumber = web3.BigNumber; + +const should = require('chai') + .use(require('chai-as-promised')) + .use(require('chai-bignumber')(BigNumber)) + .should(); + +const MintedCrowdsale = artifacts.require('MintedCrowdsaleImpl'); +const RBACMintableToken = artifacts.require('RBACMintableToken'); + +const ROLE_MINTER = 'minter'; + +contract('MintedCrowdsale using RBACMintableToken', function ([_, investor, wallet, purchaser]) { + const rate = new BigNumber(1000); + const value = ether(5); + + const expectedTokenAmount = rate.mul(value); + + beforeEach(async function () { + this.token = await RBACMintableToken.new(); + this.crowdsale = await MintedCrowdsale.new(rate, wallet, this.token.address); + await this.token.adminAddRole(this.crowdsale.address, ROLE_MINTER); + }); + + describe('accepting payments', function () { + it('should have minter role on token', async function () { + const isMinter = await this.token.hasRole(this.crowdsale.address, ROLE_MINTER); + isMinter.should.equal(true); + }); + + it('should accept payments', async function () { + await this.crowdsale.send(value).should.be.fulfilled; + await this.crowdsale.buyTokens(investor, { value: value, from: purchaser }).should.be.fulfilled; + }); + }); + + describe('high-level purchase', function () { + it('should log purchase', async function () { + const { logs } = await this.crowdsale.sendTransaction({ value: value, from: investor }); + const event = logs.find(e => e.event === 'TokenPurchase'); + should.exist(event); + event.args.purchaser.should.equal(investor); + event.args.beneficiary.should.equal(investor); + event.args.value.should.be.bignumber.equal(value); + event.args.amount.should.be.bignumber.equal(expectedTokenAmount); + }); + + it('should assign tokens to sender', async function () { + await this.crowdsale.sendTransaction({ value: value, from: investor }); + let balance = await this.token.balanceOf(investor); + balance.should.be.bignumber.equal(expectedTokenAmount); + }); + + it('should forward funds to wallet', async function () { + const pre = web3.eth.getBalance(wallet); + await this.crowdsale.sendTransaction({ value, from: investor }); + const post = web3.eth.getBalance(wallet); + post.minus(pre).should.be.bignumber.equal(value); + }); + }); +}); diff --git a/test/token/ERC20/RBACMintableToken.test.js b/test/token/ERC20/RBACMintableToken.test.js new file mode 100644 index 00000000000..ae6fadae642 --- /dev/null +++ b/test/token/ERC20/RBACMintableToken.test.js @@ -0,0 +1,68 @@ +import assertRevert from '../../helpers/assertRevert'; +const RBACMintableToken = artifacts.require('RBACMintableToken'); + +const ROLE_MINTER = 'minter'; + +contract('RBACMintable', function ([owner, anotherAccount, minter]) { + beforeEach(async function () { + this.token = await RBACMintableToken.new({ from: owner }); + await this.token.adminAddRole(minter, ROLE_MINTER); + }); + + describe('mint', function () { + const amount = 100; + + describe('when the sender has the minter role', function () { + const from = minter; + + describe('when the token minting is not finished', function () { + it('mints the requested amount', async function () { + await this.token.mint(owner, amount, { from }); + + const balance = await this.token.balanceOf(owner); + assert.equal(balance, amount); + }); + + it('emits a mint and a transfer event', async function () { + const { logs } = await this.token.mint(owner, amount, { from }); + + assert.equal(logs.length, 2); + assert.equal(logs[0].event, 'Mint'); + assert.equal(logs[0].args.to, owner); + assert.equal(logs[0].args.amount, amount); + assert.equal(logs[1].event, 'Transfer'); + }); + }); + + describe('when the token minting is finished', function () { + beforeEach(async function () { + await this.token.finishMinting({ from: owner }); + }); + + it('reverts', async function () { + await assertRevert(this.token.mint(owner, amount, { from })); + }); + }); + }); + + describe('when the sender has not the minter role', function () { + const from = anotherAccount; + + describe('when the token minting is not finished', function () { + it('reverts', async function () { + await assertRevert(this.token.mint(owner, amount, { from })); + }); + }); + + describe('when the token minting is already finished', function () { + beforeEach(async function () { + await this.token.finishMinting({ from: owner }); + }); + + it('reverts', async function () { + await assertRevert(this.token.mint(owner, amount, { from })); + }); + }); + }); + }); +});