Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
11 changes: 7 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ install :; forge install foundry-rs/forge-std --no-commit && \
forge install smartcontractkit/chainlink-brownie-contracts --no-commit && \
forge install OpenZeppelin/openzeppelin-community-contracts --no-commit
.PHONY: test
test :; forge test
test-vvv :; forge test -vvv
test-gasreport :; forge test --gas-report
test-fork :; forge test --fork-url ${ETH_RPC_URL} -vvv
test : clean
forge test
test-vvv : clean
forge test -vvv
test-gasreport : clean
forge test --gas-report
test-fork :; forge test --fork-url ${ETH_RPC_URL} -vvv
.PHONY: coverage
coverage :; mkdir -p ./coverage && forge coverage --no-match-coverage "script|test" --report lcov --report-file coverage/lcov.info && genhtml coverage/lcov.info -o coverage --branch-coverage
snapshot :; forge snapshot
Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ test = "test"
out = "out"
libs = ["lib"]
remappings = []
#ffi = true
ffi = true
ast = true
build_info = true
extra_output = ["storageLayout"]
Expand Down
3 changes: 2 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ forge-std/=lib/forge-std/src/
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/src/
@openzeppelin/community-contracts/=lib/openzeppelin-community-contracts/contracts/
@openzeppelin/community-contracts/=lib/openzeppelin-community-contracts/contracts/
@openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/
2 changes: 1 addition & 1 deletion script/DeployOracleMock.s.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.20;
pragma solidity 0.8.22;

//import {Script, console} from "forge-std/Script.sol"; // not recognized by VS Code
import {Script, console} from "lib/forge-std/src/Script.sol";
Expand Down
14 changes: 9 additions & 5 deletions script/DeployToken.s.sol
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.20;
pragma solidity 0.8.22;

//import {Script, console} from "forge-std/Script.sol"; // not recognized by VS Code
import {Script, console} from "lib/forge-std/src/Script.sol";
import {HelperConfig} from "./HelperConfig.s.sol";
import {DeployOracleMock} from "./DeployOracleMock.s.sol";
import {EmGEMxToken} from "../src/EmGEMxToken.sol";
import {MockV3Aggregator} from "../test/mocks/MockV3Aggregator.sol";
import {Upgrades} from "@openzeppelin-foundry-upgrades/Upgrades.sol";

/**
* @title Deployment script for the EmGEMx token contract
Expand All @@ -30,13 +31,16 @@ contract DeployToken is Script {
}
console.log("Oracle address:", esuOracle);

token = new EmGEMxToken();
console.log("Token address:", address(token));

string memory tokenName = vm.envString("TOKEN_NAME"); // "EmGEMx Switzerland"
string memory tokenSymbol = vm.envString("TOKEN_SYMBOL"); // "EmCH"

token.initialize(esuOracle, tokenName, tokenSymbol);
address proxyAddress = Upgrades.deployTransparentProxy(
"EmGEMxToken.sol", msg.sender, abi.encodeCall(EmGEMxToken.initialize, (esuOracle, tokenName, tokenSymbol))
);
token = EmGEMxToken(proxyAddress);
console.log("Token address:", address(token));
address implementationAddress = Upgrades.getImplementationAddress(proxyAddress);
console.log("Implementation address:", implementationAddress);

vm.stopBroadcast();

Expand Down
2 changes: 1 addition & 1 deletion script/GrantMinterRole.s.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.20;
pragma solidity 0.8.22;

//import {Script, console} from "forge-std/Script.sol"; // not recognized by VS Code
import {Script, console} from "lib/forge-std/src/Script.sol";
Expand Down
2 changes: 1 addition & 1 deletion script/HelperConfig.s.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.20;
pragma solidity 0.8.22;

import {MockV3Aggregator} from "../test/mocks/MockV3Aggregator.sol";
import {Script, console} from "forge-std/Script.sol";
Expand Down
2 changes: 1 addition & 1 deletion script/Mint.s.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.20;
pragma solidity 0.8.22;

//import {Script, console} from "forge-std/Script.sol"; // not recognized by VS Code
import {Script, console} from "lib/forge-std/src/Script.sol";
Expand Down
34 changes: 25 additions & 9 deletions src/ERC20BlocklistUpgradeable.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.20;
pragma solidity 0.8.22;

import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";

Expand All @@ -17,10 +17,23 @@ import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/
* {_unblockUser} is called.
*/
abstract contract ERC20BlocklistUpgradeable is ERC20Upgradeable {
/**
* @dev Blocked status of addresses. True if blocked, False otherwise.
*/
mapping(address user => bool) private _blocked;
/// @custom:storage-location erc7201:ERC20BlocklistUpgradeable.storage
struct ERC20BlocklistUpgradeableStorage {
/**
* @dev Blocked status of addresses. True if blocked, False otherwise.
*/
mapping(address user => bool) _blocked;
}

// keccak256(abi.encode(uint256(keccak256("ERC20BlocklistUpgradeable.storage")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant ERC20BlocklistUpgradeableStorageLocation =
0x943d6f0aae33a11bdac5b1e1fa272eeefbfbb201792466fc6e6bfcdac78e2f00;

function _getERC20BlocklistStorage() private pure returns (ERC20BlocklistUpgradeableStorage storage $) {
assembly {
$.slot := ERC20BlocklistUpgradeableStorageLocation
}
}

/**
* @dev Emitted when a user is blocked.
Expand All @@ -40,8 +53,9 @@ abstract contract ERC20BlocklistUpgradeable is ERC20Upgradeable {
/**
* @dev Returns the blocked status of an account.
*/
function blocked(address account) public virtual returns (bool) {
return _blocked[account];
function blocked(address account) public view virtual returns (bool) {
ERC20BlocklistUpgradeableStorage storage $ = _getERC20BlocklistStorage();
return $._blocked[account];
}

/**
Expand All @@ -50,7 +64,8 @@ abstract contract ERC20BlocklistUpgradeable is ERC20Upgradeable {
function _blockUser(address user) internal virtual returns (bool) {
bool isBlocked = blocked(user);
if (!isBlocked) {
_blocked[user] = true;
ERC20BlocklistUpgradeableStorage storage $ = _getERC20BlocklistStorage();
$._blocked[user] = true;
emit UserBlocked(user);
}
return isBlocked;
Expand All @@ -62,7 +77,8 @@ abstract contract ERC20BlocklistUpgradeable is ERC20Upgradeable {
function _unblockUser(address user) internal virtual returns (bool) {
bool isBlocked = blocked(user);
if (isBlocked) {
_blocked[user] = false;
ERC20BlocklistUpgradeableStorage storage $ = _getERC20BlocklistStorage();
$._blocked[user] = false;
emit UserUnblocked(user);
}
return isBlocked;
Expand Down
29 changes: 22 additions & 7 deletions src/ERC20FreezableUpgradeable.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.20;
pragma solidity 0.8.22;

import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";

Expand All @@ -20,10 +20,23 @@ import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/
* Taken from https://docs.openzeppelin.com/community-contracts/0.0.1/api/token#ERC20Custodian
*/
abstract contract ERC20FreezableUpgradeable is ERC20Upgradeable {
/**
* @dev The amount of tokens frozen by user address.
*/
mapping(address user => uint256 amount) private _frozen;
/// @custom:storage-location erc7201:ERC20FreezableUpgradeable.storage
struct ERC20FreezableUpgradeableStorage {
/**
* @dev The amount of tokens frozen by user address.
*/
mapping(address user => uint256 amount) _frozen;
}

// keccak256(abi.encode(uint256(keccak256("ERC20FreezableUpgradeable.storage")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant ERC20FreezableUpgradeableStorageLocation =
0x3fa3c15e60cd35140c36fb837aa364f49ac6961def966d04c318a640c7984100;

function _getERC20FreezableStorage() private pure returns (ERC20FreezableUpgradeableStorage storage $) {
assembly {
$.slot := ERC20FreezableUpgradeableStorageLocation
}
}

/**
* @dev Emitted when tokens are frozen for a user.
Expand Down Expand Up @@ -66,7 +79,8 @@ abstract contract ERC20FreezableUpgradeable is ERC20Upgradeable {
* @dev Returns the amount of tokens frozen for a user.
*/
function frozen(address user) public view virtual returns (uint256) {
return _frozen[user];
ERC20FreezableUpgradeableStorage storage $ = _getERC20FreezableStorage();
return $._frozen[user];
}

/**
Expand All @@ -80,7 +94,8 @@ abstract contract ERC20FreezableUpgradeable is ERC20Upgradeable {
*/
function freeze(address user, uint256 amount) external virtual onlyFreezer {
if (availableBalance(user) < amount) revert ERC20InsufficientUnfrozenBalance(user);
_frozen[user] = amount;
ERC20FreezableUpgradeableStorage storage $ = _getERC20FreezableStorage();
$._frozen[user] = amount;
emit TokensFrozen(user, amount);
}

Expand Down
46 changes: 33 additions & 13 deletions src/EmGEMxToken.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.20;
pragma solidity 0.8.22;

import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
Expand Down Expand Up @@ -70,9 +70,8 @@ contract EmGEMxToken is
- max_tokens = esu / esu_per_token
*/

// initial esuPerToken: 0.01
uint256 private esuPerTokenValue = 1;
uint256 private esuPerTokenPrecision = 100;
uint256 private esuPerTokenValue;
uint256 private esuPerTokenPrecision;

///////////////////
// Events
Expand All @@ -93,16 +92,24 @@ contract EmGEMxToken is
_;
}

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

///////////////////
// Functions
///////////////////

function initialize(address oracleAddres, string memory name, string memory symbol) public initializer {
function initialize(address oracleAddress, string memory name, string memory symbol) public initializer {
validateNotZeroAddress(oracleAddress);

__ERC20_init(name, symbol);
__ERC20Burnable_init();
__ERC20Pausable_init();
__AccessControl_init();
__Ownable_init(_msgSender());
__ERC20Permit_init(name);
_grantRole(DEFAULT_ADMIN_ROLE, _msgSender());
_setRoleAdmin(MINTER_ROLE, DEFAULT_ADMIN_ROLE);
_setRoleAdmin(ESU_PER_TOKEN_MODIFIER_ROLE, DEFAULT_ADMIN_ROLE);
Expand All @@ -111,7 +118,11 @@ contract EmGEMxToken is
_setRoleAdmin(LIMITER_ROLE, DEFAULT_ADMIN_ROLE);
_setRoleAdmin(REDEEMER_ROLE, DEFAULT_ADMIN_ROLE);

oracle = AggregatorV3Interface(oracleAddres);
oracle = AggregatorV3Interface(oracleAddress);

// initial esuPerToken: 0.01
esuPerTokenValue = 1;
esuPerTokenPrecision = 100;
}

///////////////////
Expand All @@ -124,8 +135,13 @@ contract EmGEMxToken is
}

/// @notice Wrapps ERC20BurnableUpgradeable._burn
function burn(address account, uint256 value) external onlyRole(MINTER_ROLE) {
_burn(account, value);
function burn(uint256 value) public override onlyRole(MINTER_ROLE) {
super.burn(value);
}

/// @notice Wrapps ERC20BurnableUpgradeable._burnFrom
function burnFrom(address account, uint256 value) public override onlyRole(MINTER_ROLE) {
super.burnFrom(account, value);
}

/// @notice Burns token from the redeemAddress.
Expand Down Expand Up @@ -215,6 +231,7 @@ contract EmGEMxToken is
return type(uint256).max;
}

// as returned oracle value has 8 decimals (same as token decimals) we can directly take it for token supply calculation
return _getEsuFromOracle() * esuPerTokenPrecision / esuPerTokenValue;
}

Expand Down Expand Up @@ -262,16 +279,19 @@ contract EmGEMxToken is
/// @return The quried ESU value from the chainlink PoR oracle.
function _getEsuFromOracle() private view returns (uint256) {
(
/* uint80 roundID */
,
uint80 roundID,
int256 answer,
/*uint startedAt*/
,
/*uint timeStamp*/
,
/*uint80 answeredInRound*/
uint256 updatedAt,
uint80 answeredInRound
) = oracle.latestRoundData();

require(answer > 0, "PoR: answer is zero");
require(updatedAt > 0, "PoR: updatedAt is zero");
require(answeredInRound >= roundID, "PoR: data is stale");

// according to docs answer has always 8 decimals!
return uint256(answer);
}

Expand Down
13 changes: 10 additions & 3 deletions test/mocks/MockV3Aggregator.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.20;
pragma solidity 0.8.22;

import {AggregatorV3Interface} from "@chainlink/contracts/v0.8/shared/interfaces/AggregatorV3Interface.sol";

Expand All @@ -18,6 +18,7 @@ contract MockV3Aggregator is AggregatorV3Interface {
int256 public latestAnswer;
uint256 public latestTimestamp;
uint256 public latestRound;
bool public isStale;

mapping(uint256 => int256) public getAnswer;
mapping(uint256 => uint256) public getTimestamp;
Expand All @@ -34,6 +35,7 @@ contract MockV3Aggregator is AggregatorV3Interface {
getAnswer[latestRound] = _answer;
getTimestamp[latestRound] = block.timestamp;
getStartedAt[latestRound] = block.timestamp;
isStale = false;
}

function updateRoundData(uint80 _roundId, int256 _answer, uint256 _timestamp, uint256 _startedAt) public {
Expand All @@ -43,14 +45,15 @@ contract MockV3Aggregator is AggregatorV3Interface {
getAnswer[latestRound] = _answer;
getTimestamp[latestRound] = _timestamp;
getStartedAt[latestRound] = _startedAt;
isStale = false;
}

function getRoundData(uint80 _roundId)
external
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
{
return (_roundId, getAnswer[_roundId], getStartedAt[_roundId], getTimestamp[_roundId], _roundId);
return (_roundId, getAnswer[_roundId], getStartedAt[_roundId], getTimestamp[_roundId], isStale ? 0 : _roundId);
}

function latestRoundData()
Expand All @@ -63,10 +66,14 @@ contract MockV3Aggregator is AggregatorV3Interface {
getAnswer[latestRound],
getStartedAt[latestRound],
getTimestamp[latestRound],
uint80(latestRound)
isStale ? 0 : uint80(latestRound)
);
}

function setStale() external {
isStale = true;
}

function description() external pure returns (string memory) {
return "v0.6/tests/MockV3Aggregator.sol";
}
Expand Down
Loading
Loading