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

[TERA-168] Add burner role for burn functions #24

Merged
merged 10 commits into from
Oct 1, 2024
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: 119873)
FiatTokenV2Test:testBurnByBurner() (gas: 68300)
FiatTokenV2Test:testBurnByBurnerMustBeLessThanBalance() (gas: 86473)
FiatTokenV2Test:testBurnByBurnerUnauthorized() (gas: 88516)
FiatTokenV2Test:testBurnerPause() (gas: 55503)
FiatTokenV2Test:testGrantBurnerRole() (gas: 51863)
FiatTokenV2Test:testRenounceBurnerRole() (gas: 41034)
FiatTokenV2Test:testRevokeBurnerRole() (gas: 41436)
FiatTokenV2Test:testUnblacklistBurner() (gas: 95652)
FiatTokenV2Test:testUpgradeToAndCall() (gas: 5104336)
FiatTokenV2Test:testVersion() (gas: 12000)
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("FiatTokenV1 upgraded to FiatTokenV2");
vm.stopBroadcast();
}
}
17 changes: 17 additions & 0 deletions src/v2/FiatTokenV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// 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");

function burnByBurner(uint256 value) public 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.burnByBurner(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.burnByBurner(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.burnByBurner(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.burnByBurner(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.burnByBurner(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.burnByBurner(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");
}
}
Loading