Skip to content
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
130 changes: 130 additions & 0 deletions contracts/ERC1155WithERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

/**
* @title ERC1155WithERC20
* @dev ERC1155 token where token ID 0 also implements the ERC20 interface.
*/
contract ERC1155WithERC20 is ERC1155Supply, IERC20, AccessControl {
string public name;
string public symbol;
uint8 public constant decimals = 18;

mapping(address => mapping(address => uint256)) private _allowances;
uint256 private _totalSupply;

bytes32 public constant BRAND_ADMIN_ROLE = keccak256("BRAND_ADMIN_ROLE");
bytes32 public constant BRAND_MINTER_ROLE = keccak256("BRAND_MINTER_ROLE");

mapping(uint256 => address) private _tokenBrands;

constructor(string memory name_, string memory symbol_, string memory uri_) ERC1155(uri_) {
name = name_;
symbol = symbol_;
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
}

function brandRole(bytes32 role, address brand) public pure returns (bytes32) {
return keccak256(abi.encodePacked(role, brand));
}

function createBrand(address brand, address admin) external onlyRole(DEFAULT_ADMIN_ROLE) {
bytes32 adminRole = brandRole(BRAND_ADMIN_ROLE, brand);
bytes32 minterRole = brandRole(BRAND_MINTER_ROLE, brand);
_setRoleAdmin(minterRole, adminRole);
_grantRole(adminRole, admin);
}

function grantBrandRole(address brand, bytes32 role, address account) external onlyRole(brandRole(BRAND_ADMIN_ROLE, brand)) {
_grantRole(brandRole(role, brand), account);
}

function revokeBrandRole(address brand, bytes32 role, address account) external onlyRole(brandRole(BRAND_ADMIN_ROLE, brand)) {
_revokeRole(brandRole(role, brand), account);
}

function assignTokenToBrand(uint256 id, address brand) external onlyRole(DEFAULT_ADMIN_ROLE) {
_tokenBrands[id] = brand;
}

function brandOf(uint256 id) public view returns (address) {
return _tokenBrands[id];
}

// ERC20
function totalSupply() public view override returns (uint256) {
return _totalSupply;
}

function balanceOf(address account) public view override(IERC20) returns (uint256) {
return super.balanceOf(account, 0);
}

function transfer(address to, uint256 amount) public override returns (bool) {
_safeTransferFrom(msg.sender, to, 0, amount, "");
return true;
}

function allowance(address owner, address spender) public view override returns (uint256) {
return _allowances[owner][spender];
}

function approve(address spender, uint256 amount) public override returns (bool) {
_allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}

function transferFrom(address from, address to, uint256 amount) public override returns (bool) {
uint256 currentAllowance = _allowances[from][msg.sender];
require(currentAllowance >= amount, "ERC20: insufficient allowance");
unchecked {
_allowances[from][msg.sender] = currentAllowance - amount;
}
_safeTransferFrom(from, to, 0, amount, "");
return true;
}

// Minting and burning
function mint(address to, uint256 id, uint256 amount, bytes memory data) public {
address brand = _tokenBrands[id];
require(brand != address(0), "ERC1155WithERC20: brand not set");
require(hasRole(brandRole(BRAND_MINTER_ROLE, brand), msg.sender), "ERC1155WithERC20: missing brand minter role");
if (id == 0) {
_totalSupply += amount;
}
_mint(to, id, amount, data);
}

function burn(address from, uint256 id, uint256 amount) public {
require(from == msg.sender || isApprovedForAll(from, msg.sender), "ERC1155: caller is not owner nor approved");
if (id == 0) {
_totalSupply -= amount;
}
_burn(from, id, amount);
}

function _beforeTokenTransfer(
address operator,
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) internal override(ERC1155Supply) {
super._beforeTokenTransfer(operator, from, to, ids, amounts, data);
}

function supportsInterface(bytes4 interfaceId)
public
view
override(ERC1155, AccessControl)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
101 changes: 101 additions & 0 deletions test/ERC1155WithERC20.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("ERC1155WithERC20", function () {
beforeEach(async function () {
[
this.deployer,
this.brand,
this.minter,
this.addr1,
this.addr2,
this.addr3,
] = await ethers.getSigners();
const Token = await ethers.getContractFactory("ERC1155WithERC20");
this.token = await Token.deploy("Token", "TKN", "uri/");
await this.token.deployed();

await this.token.createBrand(this.brand.address, this.brand.address);
await this.token.assignTokenToBrand(0, this.brand.address);
await this.token.assignTokenToBrand(1, this.brand.address);
await this.token
.connect(this.brand)
.grantBrandRole(
this.brand.address,
await this.token.BRAND_MINTER_ROLE(),
this.minter.address
);
});

it("requires brand minter role to mint", async function () {
expect(await this.token.brandOf(0)).to.equal(this.brand.address);

await expect(
this.token.mint(this.addr1.address, 0, 1, "0x")
).to.be.revertedWith("ERC1155WithERC20: missing brand minter role");

await this.token
.connect(this.minter)
.mint(this.addr1.address, 0, 1, "0x");
expect(
await this.token["balanceOf(address)"](this.addr1.address)
).to.equal(1);
});

it("mints id 0 as ERC20 and tracks supply", async function () {
await this.token
.connect(this.minter)
.mint(this.addr1.address, 0, 100, "0x");
expect(await this.token["totalSupply()"]()).to.equal(100);
expect(await this.token["totalSupply(uint256)"](0)).to.equal(100);
expect(await this.token["balanceOf(address)"](this.addr1.address)).to.equal(100);
expect(await this.token["balanceOf(address,uint256)"](this.addr1.address, 0)).to.equal(100);
});

it("handles ERC20 transfers and allowances for id 0", async function () {
await this.token
.connect(this.minter)
.mint(this.addr1.address, 0, 100, "0x");

await this.token.connect(this.addr1).transfer(this.addr2.address, 40);
expect(await this.token["balanceOf(address)"](this.addr1.address)).to.equal(60);
expect(await this.token["balanceOf(address)"](this.addr2.address)).to.equal(40);

await this.token.connect(this.addr1).approve(this.addr2.address, 50);
expect(await this.token.allowance(this.addr1.address, this.addr2.address)).to.equal(50);

await this.token.connect(this.addr2).transferFrom(this.addr1.address, this.addr3.address, 30);
expect(await this.token["balanceOf(address)"](this.addr1.address)).to.equal(30);
expect(await this.token["balanceOf(address)"](this.addr3.address)).to.equal(30);
expect(await this.token.allowance(this.addr1.address, this.addr2.address)).to.equal(20);
});

it("burns id 0 and updates ERC20 supply", async function () {
await this.token
.connect(this.minter)
.mint(this.addr1.address, 0, 50, "0x");
await this.token.connect(this.addr1).burn(this.addr1.address, 0, 20);
expect(await this.token["totalSupply()"]()).to.equal(30);
expect(await this.token["totalSupply(uint256)"](0)).to.equal(30);
expect(await this.token["balanceOf(address)"](this.addr1.address)).to.equal(30);
});

it("processes non-zero ids as ERC1155 tokens", async function () {
await this.token
.connect(this.minter)
.mint(this.addr1.address, 1, 10, "0x");
expect(await this.token["totalSupply()"]()).to.equal(0);
expect(await this.token["totalSupply(uint256)"](1)).to.equal(10);
expect(await this.token["balanceOf(address,uint256)"](this.addr1.address, 1)).to.equal(10);

await this.token.connect(this.addr1).safeTransferFrom(this.addr1.address, this.addr2.address, 1, 3, "0x");
expect(await this.token["balanceOf(address,uint256)"](this.addr1.address, 1)).to.equal(7);
expect(await this.token["balanceOf(address,uint256)"](this.addr2.address, 1)).to.equal(3);

await this.token.connect(this.addr1).burn(this.addr1.address, 1, 2);
expect(await this.token["balanceOf(address,uint256)"](this.addr1.address, 1)).to.equal(5);
expect(await this.token["totalSupply(uint256)"](1)).to.equal(8);
expect(await this.token["totalSupply()"]()).to.equal(0);
});
});