Skip to content

Commit

Permalink
Merge pull request #24 from trillion-network/add-burner-role
Browse files Browse the repository at this point in the history
[TERA-168] Add burner role for burn functions
  • Loading branch information
stanleygtrillion authored Oct 1, 2024
2 parents c620158 + c72bab6 commit 5422f36
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 7 deletions.
11 changes: 11 additions & 0 deletions .gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,15 @@ FiatTokenV1Test:testUnpause() (gas: 35210)
FiatTokenV1Test:testUnpauseUnauthorized() (gas: 52752)
FiatTokenV1Test:testUpgradeToAndCall() (gas: 2336296)
FiatTokenV1Test:testVersion() (gas: 11934)
FiatTokenV2Test:testBlacklistBurner() (gas: 120114)
FiatTokenV2Test:testBurnByBurner() (gas: 68598)
FiatTokenV2Test:testBurnByBurnerMustBeLessThanBalance() (gas: 86736)
FiatTokenV2Test:testBurnByBurnerUnauthorized() (gas: 88757)
FiatTokenV2Test:testBurnerPause() (gas: 55480)
FiatTokenV2Test:testGrantBurnerRole() (gas: 52173)
FiatTokenV2Test:testRenounceBurnerRole() (gas: 41264)
FiatTokenV2Test:testRevokeBurnerRole() (gas: 41667)
FiatTokenV2Test:testUnblacklistBurner() (gas: 95862)
FiatTokenV2Test:testUpgradeToAndCall() (gas: 5104270)
FiatTokenV2Test:testVersion() (gas: 11978)
RescuableV1Test:testRescue() (gas: 42236)
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "@trillion-x/trillion-contracts",
"name": "trillion-contracts",
"version": "1.0.0",
"license": "MIT",
"repository": "[email protected]:royal-markets/royal-contracts.git",
"repository": "[email protected]:trillion-network/trillion-contracts.git",
"files": [
"out",
"ts-types"
Expand Down
6 changes: 3 additions & 3 deletions script/UpgradeFiatToken.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ pragma solidity ^0.8.20;
import "forge-std/console2.sol";
import {Script} from "forge-std/Script.sol";
import {Upgrades, Options} from "openzeppelin-foundry-upgrades/Upgrades.sol";
import {FiatTokenV1} from "../src/v1/FiatTokenV1.sol";
import {FiatTokenV2} from "../src/v2/FiatTokenV2.sol";

contract DeployFiatToken is Script {
contract UpgradeFiatToken is Script {
function run() public {
vm.startBroadcast();
address fiatTokenProxyAddress = vm.envAddress("FIAT_TOKEN_PROXY_ADDRESS");
Expand All @@ -16,7 +16,7 @@ contract DeployFiatToken is Script {
opts.referenceContract = "FiatTokenV1.sol";
// upgrade contract
Upgrades.upgradeProxy(fiatTokenProxyAddress, "FiatTokenV2.sol", "", opts);
console2.log("FiatTokenV1 upgraded to version 2");
console2.log("FiatToken upgrade successful");
vm.stopBroadcast();
}
}
19 changes: 19 additions & 0 deletions src/v2/FiatTokenV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import "../v1/FiatTokenV1.sol";

/// @custom:security-contact [email protected]
contract FiatTokenV2 is FiatTokenV1 {
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

// burnByBurnerOnly is a token burn operation that can only be called by BURNER_ROLE
// this is to separate the operation from MINTER_ROLE that can call mint and burn function
function burnByBurnerOnly(uint256 value) public virtual onlyRole(BURNER_ROLE) {
_burn(_msgSender(), value);
}

function version() public pure virtual override(FiatTokenV1) returns (string memory) {
return "2";
}
}
3 changes: 1 addition & 2 deletions test/v1/FiatTokenV1.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.so
import {ERC20CappedUpgradeable} from
"@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20CappedUpgradeable.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {Upgrades, Options} from "openzeppelin-foundry-upgrades/Upgrades.sol";
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {CallerBlacklisted} from "../../src/v1/BlacklistableV1.sol";
import {Ramen} from "../../src/mocks/Ramen.sol";
// import {FiatTokenV99} from "../../src/mocks/FiatTokenV99.sol";

// mock contract to test upgrades
contract FiatTokenV99 is FiatTokenV1 {
Expand Down
247 changes: 247 additions & 0 deletions test/v2/FiatTokenV2.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol";

import {Test, console2} from "forge-std/Test.sol";
import {FiatTokenV1} from "../../src/v1/FiatTokenV1.sol";
import {FiatTokenV2} from "../../src/v2/FiatTokenV2.sol";

import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol";
import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {CallerBlacklisted} from "../../src/v1/BlacklistableV1.sol";

contract FiatTokenV2Test is Test {
FiatTokenV2 public fiatTokenV2;
ERC1967Proxy public proxy;
address public owner;
address public defaultAdmin;
address public pauser;
address public minter;
address public upgrader;
address public rescuer;
address public blacklister;
address public unauthorized;
address public burner;
address public trustedAddress;
string public tokenName = "FiatTokenV2";
string public tokenSymbol = "FIAT";

// events
event Blacklisted(address indexed account);
event UnBlacklisted(address indexed account);

function setUp() public {
owner = address(this);
defaultAdmin = vm.addr(1);
pauser = vm.addr(2);
minter = vm.addr(3);
upgrader = vm.addr(4);
rescuer = vm.addr(5);
blacklister = vm.addr(6);
unauthorized = vm.addr(7);
burner = vm.addr(8);
trustedAddress = address(0x66787300CCc33F17643a02635ca96d54301aE2a8);

// Deploy the token implementation
fiatTokenV2 = new FiatTokenV2();

// Deploy the proxy and initialize the contract through the proxy
vm.prank(trustedAddress);
proxy = new ERC1967Proxy(
address(fiatTokenV2),
abi.encodeCall(
fiatTokenV2.initialize,
(defaultAdmin, pauser, minter, upgrader, rescuer, blacklister, tokenName, tokenSymbol)
)
);

// Attach the FiatTokenV2 interface to the deployed proxy
fiatTokenV2 = FiatTokenV2(address(proxy));

// Assign BURNER_ROLE to burner
bytes32 burnerRole = fiatTokenV2.BURNER_ROLE();
vm.prank(defaultAdmin);
fiatTokenV2.grantRole(burnerRole, burner);
}

// ERC 20 behavior

function testVersion() public {
assertEq(fiatTokenV2.version(), "2");
}

function testBurnByBurner() public {
assertEq(fiatTokenV2.totalSupply(), 0);
vm.prank(minter);
fiatTokenV2.mint(burner, 100);
assertEq(fiatTokenV2.totalSupply(), 100);
assertEq(fiatTokenV2.balanceOf(burner), 100);
vm.prank(burner);
fiatTokenV2.burnByBurnerOnly(100);
assertEq(fiatTokenV2.totalSupply(), 0);
assertEq(fiatTokenV2.balanceOf(burner), 0);
}

function testBurnByBurnerMustBeLessThanBalance() public {
assertEq(fiatTokenV2.totalSupply(), 0);
vm.prank(minter);
fiatTokenV2.mint(burner, 100);
assertEq(fiatTokenV2.totalSupply(), 100);
assertEq(fiatTokenV2.balanceOf(burner), 100);
vm.expectRevert(
abi.encodeWithSelector(
IERC20Errors.ERC20InsufficientBalance.selector,
burner, // from
100, // fromBalance
101 // value
)
);
vm.prank(burner);
fiatTokenV2.burnByBurnerOnly(101);
}

function testBurnByBurnerUnauthorized() public {
assertEq(fiatTokenV2.totalSupply(), 0);
vm.prank(minter);
fiatTokenV2.mint(owner, 100);
assertEq(fiatTokenV2.totalSupply(), 100);
assertEq(fiatTokenV2.balanceOf(owner), 100);
vm.expectRevert(
abi.encodeWithSelector(
IAccessControl.AccessControlUnauthorizedAccount.selector,
unauthorized, // address
fiatTokenV2.BURNER_ROLE() // role
)
);
vm.prank(unauthorized);
fiatTokenV2.burnByBurnerOnly(100);
}

function testBurnerPause() public {
assertEq(fiatTokenV2.paused(), false);
vm.prank(pauser);
fiatTokenV2.pause();
assertEq(fiatTokenV2.paused(), true);
// when contract is paused, not allowed to burn
vm.expectRevert(Pausable.EnforcedPause.selector);
vm.prank(burner);
fiatTokenV2.burnByBurnerOnly(100);
}

function testBlacklistBurner() public {
// mint tokens to burner account
// for simplicity, we blacklist the burner account since it has permissions to burn
vm.prank(minter);
fiatTokenV2.mint(burner, 100);
assertEq(fiatTokenV2.balanceOf(burner), 100);

// blacklist minter account
assertEq(fiatTokenV2.isBlacklisted(burner), false);
vm.expectEmit();
emit Blacklisted(burner);
vm.prank(blacklister);
fiatTokenV2.blacklist(burner);
assertEq(fiatTokenV2.isBlacklisted(burner), true);
// once blacklisted, not allowed to transfer
vm.expectRevert(abi.encodeWithSelector(CallerBlacklisted.selector, burner));
vm.prank(burner);
fiatTokenV2.transfer(burner, 100);
// not allowed to burn
vm.expectRevert(abi.encodeWithSelector(CallerBlacklisted.selector, burner));
vm.prank(burner);
fiatTokenV2.burnByBurnerOnly(100);
}

function testUnblacklistBurner() public {
// mint 100 for to burner first
vm.prank(minter);
fiatTokenV2.mint(burner, 100);
// blacklist burner account
assertEq(fiatTokenV2.isBlacklisted(burner), false);
vm.expectEmit();
emit Blacklisted(burner);
vm.prank(blacklister);
fiatTokenV2.blacklist(burner);
assertEq(fiatTokenV2.isBlacklisted(burner), true);
// unblacklist burner account
vm.expectEmit();
emit UnBlacklisted(burner);
vm.prank(blacklister);
fiatTokenV2.unBlacklist(burner);
assertEq(fiatTokenV2.isBlacklisted(burner), false);
// once unblacklisted, allowed to burn
vm.prank(burner);
fiatTokenV2.burnByBurnerOnly(100);
// no balance left after transferring and burning
assertEq(fiatTokenV2.balanceOf(burner), 0);
}

// Access control

function testGrantBurnerRole() public {
bytes32 burnerRole = fiatTokenV2.BURNER_ROLE();
assertEq(fiatTokenV2.hasRole(burnerRole, unauthorized), false);
vm.prank(defaultAdmin);
fiatTokenV2.grantRole(burnerRole, unauthorized);
assertEq(fiatTokenV2.hasRole(burnerRole, unauthorized), true);
}

function testRevokeBurnerRole() public {
bytes32 burnerRole = fiatTokenV2.BURNER_ROLE();
vm.prank(defaultAdmin);
fiatTokenV2.grantRole(burnerRole, unauthorized);
assertEq(fiatTokenV2.hasRole(burnerRole, unauthorized), true);

vm.prank(defaultAdmin);
fiatTokenV2.revokeRole(burnerRole, unauthorized);
assertEq(fiatTokenV2.hasRole(burnerRole, unauthorized), false);
}

function testRenounceBurnerRole() public {
bytes32 burnerRole = fiatTokenV2.BURNER_ROLE();
vm.prank(defaultAdmin);
fiatTokenV2.grantRole(burnerRole, unauthorized);
assertEq(fiatTokenV2.hasRole(burnerRole, unauthorized), true);

vm.prank(unauthorized); // caller needs to be the one renouncing their own role
fiatTokenV2.renounceRole(burnerRole, unauthorized);
assertEq(fiatTokenV2.hasRole(burnerRole, unauthorized), false);
}

// Upgradeability

function testUpgradeToAndCall() public {
// new implementation contract
FiatTokenV1 fiatTokenV1 = new FiatTokenV1();
vm.prank(trustedAddress);
ERC1967Proxy proxyV1 = new ERC1967Proxy(
address(fiatTokenV1),
abi.encodeCall(
fiatTokenV1.initialize,
(defaultAdmin, pauser, minter, upgrader, rescuer, blacklister, tokenName, tokenSymbol)
)
);

// Attach the FiatTokenV1 interface to the deployed proxy
fiatTokenV1 = FiatTokenV1(address(proxyV1));

FiatTokenV2 fiatTokenV2New = new FiatTokenV2();
address newImplementationAddress = address(fiatTokenV2New);
assertEq(fiatTokenV1.version(), "1");
assertEq(fiatTokenV2New.version(), "2");

// upgrade contract
vm.prank(upgrader);
fiatTokenV1.upgradeToAndCall(newImplementationAddress, "");
address updatedImplementationAddress = Upgrades.getImplementationAddress(address(proxyV1));
// verify implementation address is updated
assertEq(newImplementationAddress, updatedImplementationAddress);
// verify version() function implementation is updated
assertEq(fiatTokenV1.version(), "2");
}
}

0 comments on commit 5422f36

Please sign in to comment.