Skip to content
Merged
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
139 changes: 110 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,115 @@
# EmGEMx Token

| Property | Value |
| ------------------------- | ------------------------------------------- |
| Name | EmGEMx Switzerland |
| Symbol | EmCH |
| Issuer | GemX AG, Zug, CH |
| Number of Tokens | Variable |
| Number of Decimals | 18 |
| Type | Crypto Asset (Asset Token) |
| Use Case | Tokenized Emeralds |
| Underlying Asset | Emeralds |
| Transferable | Yes |
| Transaction Fee | No |
| Burn Fee | No |
| Initial Price | Depends on ESU |
| Distribution | Proof-of-Reserve + Buy/DEX/CEX |
| Technical Base | ERC-20 on Avalanche |
| Public Tradeable | Yes |
| Governance Function | No |
| Allowlist | No |
| Blocklist | Yes |
| Mintable | Yes (redeem) |
| Burnable | Yes (redeem) |
| Pausable | Yes (all) |
| Roles | Owner, Minter, Redeem |
| Force Transfer (Clawback) | Yes/No (TBD) |
| Max Tokens per Address | No limit |
| Upgradeable | Yes |
| Cross-Chain | Yes (Ethereum, etc.) – CCIP |
| Other features | Emerald Standard Unit, Minting based on PoR |
## Token Properties

| Property | Value |
| ------------------------- | ------------------------------------------------- |
| Name | EmGEMx Switzerland |
| Symbol | EmCH |
| Issuer | GemX AG, Zug, CH |
| Number of Tokens | Variable |
| Number of Decimals | 8 |
| Type | Crypto Asset (Asset Token) |
| Use Case | Tokenized Emeralds |
| Underlying Asset | Emeralds |
| Transferable | Yes |
| Transaction Fee | No |
| Burn Fee | No |
| Initial Price | Depends on ESU |
| Distribution | Proof-of-Reserve + Buy/DEX/CEX |
| Technical Base | ERC-20 on Avalanche |
| Public Tradeable | Yes |
| Governance Function | No |
| Allowlist | No |
| Blocklist | Yes |
| Mintable | Yes |
| Burnable | Yes (redeem) |
| Pausable | Yes (all) |
| Roles | Owner, Minter, ESU mod, Pause, Custodian, Limiter |
| Force Transfer (Clawback) | No |
| Max Tokens per Address | No limit |
| Upgradeable | Yes |
| Cross-Chain | Yes (Ethereum, etc.) – CCIP |
| Other features | Emerald Standard Unit, Minting based on PoR |

Special Features
- Oracle writes Proof-of-Reserve to Blockchain (how many gemstones in ESU are in vault and can be minted)
- Mint function is limited to Proof-of-Reserve (amount of stones) and Emerald Standard Unit (ESU)
- Redeem must be transparent and results in burning of Token
- ESU changes about 0.1% per month (reduction, which results in more tokens to be allowed to be minted)

Roles
- Admin: owner of the contract, allowed to upgrade the contract, change parameters and assign/revoke roles
- Minter: allowed to mint and burn tokens
- ESU per Token Modifier: allowed to update the ESU per token
- Pauser: allowed to pause/unpause tokens
- Freezer: allowed to freeze/unfreeze tokens
- Limiter: allowed to block/unblock users
- Redeemer: allowed to burn tokens from redeem address

## Functional Requirements

The token may be deployed to multiple blockchain networks, however the core logic of the token (e.g. max supply restriction via PoR oracle, redeem functionality) stays on the `parent chain` which in fact is the **Avalanche C-Chain**.

### ESU

The `ESU` (emerald standard unit) is a value defining how many gemstones are in vault and hence can be minted (based on the `esu_per_token` parameter). Its value is maintained & confirmed/attested by auditors and brought on-chain by an Chainlink PoR oracle data feed.

- ESU value is written by chainlink
- Token has an esu_per_token value (set by emgemx)
- esu_per_token value is updated every month
- max_tokens = esu / esu_per_token

Example calculation:

1st of March:

- ESU = 2521,13
- esu_per_token = 0,01
- max_tokens = 252.113

1st of April:

- ESU = 2521,13
- esu_per_token = 0.0099
- max_tokens = 254.659,59 => hence ~2.546 new tokens are allowed to be minted compared to previous month

1st of May:

- ESU = 3871,13 (new stones delivered worth 1350 ESU)
- esu_per_token = 0,009801
- max_tokens = 394.972,96

**Monthly amount adjustment**

The following will be done once a month and influences the amount of tokens that are allowed to be minted:

1. Redeem: All redeems are executed and those tokens are burned.

2. ESU adjustment: ESU will be reduced bei 0.1%, allowing us to mint more tokens.

### Redeeming tokens

Users can redeem their tokens for the physical gemstones counterparts, which basically means that the gemstones are taken out of the safe/vault and delivered to the users in exchange for the tokens which eventually get burned. For this the user needs to transfer the tokens to a particular `redeemAddress` specified by emGEMx company and definable inside the token contract. On the parent chain tokens can only be burnt from that special address.

### Burning tokens

In general token burns should be strictly restricted, neither users should be able nor emGEMx on behalf of users should be able to burn tokens from the users (e.g. for a potential clawback scenario in case users lose access to the funds).

However certain functionality requires token burning capabilities. Hence tokens should be burnable solely in the following cases:
- **on child chains:** only by the CCIP bridge to bring tokens from the child chain (burn) to the parent chain (release). Any tokens previously transfered from parent chain (lock) to child chain (mint) do not require burning for the transfer to settle.
- **on the parent chain:** only as part of the redemption process -> and only by the redeemer from the `redeemAddress`.

### Cross Chain Support

Cross-chain support will be enabled by leveraging Chainlink's CCIP via Token Manager which allows full cross-chain capabilities without changes in the token design/implementation. The only requirement from chainlink is to have a dedicated token owner (implemented via openzeppelin's `Ownable` contract);

Cross-chain token transfers strategies used:

- Source/Parent chain (Avalanche C-Chain): `lock & release`

- All other destination/child chains (e.g. Ethereum Mainnet): `mint & burn`


## Build, Test, Deploy

Expand Down
2 changes: 1 addition & 1 deletion lib/openzeppelin-contracts
31 changes: 31 additions & 0 deletions script/DeployOracleMock.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.20;

//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 {MockV3Aggregator} from "../test/mocks/MockV3Aggregator.sol";

/**
* @title Deployment script for the chainlink data feed oracle mock.
*/
contract DeployOracleMock is Script {
function run() public returns (MockV3Aggregator) {
HelperConfig helperConfig = new HelperConfig();
uint256 mockValue = helperConfig.PROOF_OF_RESERVE_MOCK();

vm.startBroadcast();

MockV3Aggregator mock = deploy(mockValue);

vm.stopBroadcast();

return mock;
}

function deploy(uint256 mockValue) public returns (MockV3Aggregator) {
MockV3Aggregator mock = new MockV3Aggregator(int256(mockValue));
console.log("Oracle mock deployed at: ", address(mock));
return mock;
}
}
19 changes: 10 additions & 9 deletions script/DeployToken.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ pragma solidity 0.8.20;
//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";

/**
* @title Deployment script for the EmGEMx token contract
* @dev Upgradable ERC 20 contract that contains a chain switch due to the fact that certain functionality (e.g. token max supply) should be limited to the parent chain (Avalanche C-Chain).
*/
contract DeployToken is Script {
EmGEMxToken public token;

Expand All @@ -15,10 +20,12 @@ contract DeployToken is Script {

vm.startBroadcast();

(address esuOracle) = helperConfig.activeNetworkConfig();
if (esuOracle == address(0x0)) {
(address esuOracle, bool deployOracleMock) = helperConfig.activeNetworkConfig();
if (esuOracle == address(0x0) && deployOracleMock) {
uint256 mockValue = helperConfig.PROOF_OF_RESERVE_MOCK();
MockV3Aggregator mock = createProofOrReserveMock(mockValue);

DeployOracleMock deployMock = new DeployOracleMock();
MockV3Aggregator mock = deployMock.deploy(mockValue);
esuOracle = address(mock);
}
console.log("Oracle address:", esuOracle);
Expand All @@ -35,10 +42,4 @@ contract DeployToken is Script {

return token;
}

function createProofOrReserveMock(uint256 reserve) private returns (MockV3Aggregator) {
MockV3Aggregator mock = new MockV3Aggregator(int256(reserve));
console.log("Oracle mock deployed at:", address(mock));
return mock;
}
}
15 changes: 9 additions & 6 deletions script/HelperConfig.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ contract HelperConfig is Script {

struct NetworkConfig {
address esuOracle;
bool deployOracleMock;
}

constructor() {
Expand All @@ -33,21 +34,23 @@ contract HelperConfig is Script {

function getSepoliaEthConfig() public pure returns (NetworkConfig memory sepoliaNetworkConfig) {
// no oracle on other chains than Avalanche
sepoliaNetworkConfig = NetworkConfig({esuOracle: 0x8D26D407ebed4D03dE7c18f5Db913155a4D587AE});
sepoliaNetworkConfig =
NetworkConfig({esuOracle: 0x8D26D407ebed4D03dE7c18f5Db913155a4D587AE, deployOracleMock: false});
}

function getFujiEthConfig() public pure returns (NetworkConfig memory fujiNetworkConfig) {
fujiNetworkConfig = NetworkConfig({esuOracle: 0x8F1C8888fBcd9Cc5D732df1e146d399a21899c22});
fujiNetworkConfig =
NetworkConfig({esuOracle: 0x8F1C8888fBcd9Cc5D732df1e146d399a21899c22, deployOracleMock: false});
}

function getAvalancheEthConfig() public pure returns (NetworkConfig memory avalancheNetworkConfig) {
revert("Feed address missing");
avalancheNetworkConfig = NetworkConfig({esuOracle: address(0x0)});
revert("Oracle feed address missing");
avalancheNetworkConfig = NetworkConfig({esuOracle: address(0x0), deployOracleMock: false});
}

function getMainnetEthConfig() public pure returns (NetworkConfig memory mainnetNetworkConfig) {
// no oracle on other chains than Avalanche
mainnetNetworkConfig = NetworkConfig({esuOracle: address(0x0)});
mainnetNetworkConfig = NetworkConfig({esuOracle: address(0x0), deployOracleMock: false});
}

function getOrCreateAnvilEthConfig() public view returns (NetworkConfig memory anvilNetworkConfig) {
Expand All @@ -59,6 +62,6 @@ contract HelperConfig is Script {
//MockV3Aggregator mock = new MockV3Aggregator(PROOF_OF_RESERVE_MOCK);
//console.log("Anvil oracle mock:", address(mock));
//anvilNetworkConfig = NetworkConfig({esuOracle: address(mock)});
anvilNetworkConfig = NetworkConfig({esuOracle: address(0x0)});
anvilNetworkConfig = NetworkConfig({esuOracle: address(0x0), deployOracleMock: true});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/
* The frozen balance is not available for transfers or approvals
* to other entities to operate on its behalf if. The frozen balance
* can be reduced by calling {freeze} again with a lower amount.
*
* Taken from https://docs.openzeppelin.com/community-contracts/0.0.1/api/token#ERC20Custodian
*/
abstract contract ERC20CustodianUpgradeable is ERC20Upgradeable {
abstract contract ERC20FreezableUpgradeable is ERC20Upgradeable {
/**
* @dev The amount of tokens frozen by user address.
*/
Expand Down Expand Up @@ -48,15 +50,15 @@ abstract contract ERC20CustodianUpgradeable is ERC20Upgradeable {
error ERC20InsufficientFrozenBalance(address user);

/**
* @dev Error thrown when a non-custodian account attempts to perform a custodian-only operation.
* @dev Error thrown when a non-freezer account attempts to perform the freezer operation.
*/
error ERC20NotCustodian();
error ERC20NotFreezer();

/**
* @dev Modifier to restrict access to custodian accounts only.
* @dev Modifier to restrict access to freezer accounts only.
*/
modifier onlyCustodian() {
if (!_isCustodian(_msgSender())) revert ERC20NotCustodian();
modifier onlyFreezer() {
if (!_isFreezer(_msgSender())) revert ERC20NotFreezer();
_;
}

Expand All @@ -76,7 +78,7 @@ abstract contract ERC20CustodianUpgradeable is ERC20Upgradeable {
*
* - The user must have sufficient unfrozen balance.
*/
function freeze(address user, uint256 amount) external virtual onlyCustodian {
function freeze(address user, uint256 amount) external virtual onlyFreezer {
if (availableBalance(user) < amount) revert ERC20InsufficientUnfrozenBalance(user);
_frozen[user] = amount;
emit TokensFrozen(user, amount);
Expand All @@ -92,11 +94,11 @@ abstract contract ERC20CustodianUpgradeable is ERC20Upgradeable {
}

/**
* @dev Checks if the user is a custodian.
* @dev Checks if the user is a freezer.
* @param user The address of the user to check.
* @return True if the user is authorized, false otherwise.
*/
function _isCustodian(address user) internal view virtual returns (bool);
function _isFreezer(address user) internal view virtual returns (bool);

function _update(address from, address to, uint256 value) internal virtual override {
if (from != address(0) && availableBalance(from) < value) revert ERC20InsufficientUnfrozenBalance(from);
Expand Down
Loading
Loading