diff --git a/content/tron-contracts/access-control.mdx b/content/tron-contracts/access-control.mdx new file mode 100644 index 00000000..08dde6ca --- /dev/null +++ b/content/tron-contracts/access-control.mdx @@ -0,0 +1,610 @@ +--- +title: "Access Control" +--- + +Access control—that is, "who is allowed to do this thing"—is incredibly important in the world of smart contracts. The access control of your contract may govern who can mint tokens, vote on proposals, freeze transfers, and many other things. It is therefore **critical** to understand how you implement it, lest someone else [steals your whole system](https://blog.openzeppelin.com/on-the-parity-wallet-multisig-hack-405a8c12e8f7). + +## Ownership and `Ownable` + +The most common and basic form of access control is the concept of _ownership_: there's an account that is the `owner` of a contract and can do administrative tasks on it. This approach is perfectly reasonable for contracts that have a single administrative user. + +OpenZeppelin Contracts provides `Ownable` for implementing ownership in your contracts. + +```solidity +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Ownable} from "@openzeppelin/tron-contracts/access/Ownable.sol"; + +contract MyContract is Ownable { + constructor(address initialOwner) Ownable(initialOwner) {} + + function normalThing() public { + // anyone can call this normalThing() + } + + function specialThing() public onlyOwner { + // only the owner can call specialThing()! + } +} +``` + +At deployment, the `owner` of an `Ownable` contract is set to the provided `initialOwner` parameter. + +Ownable also lets you: + +- `transferOwnership` from the owner account to a new one, and +- `renounceOwnership` for the owner to relinquish this administrative privilege, a common pattern after an initial stage with centralized administration is over. + + +Removing the owner altogether will mean that administrative tasks that are protected by `onlyOwner` will no longer be callable! + + +Ownable is a simple and effective way to implement access control, but you should be mindful of the dangers associated with transferring the ownership to an incorrect account that can't interact with this contract anymore. An alternative to this problem is using `Ownable2Step`; a variant of Ownable that requires the new owner to explicitly accept the ownership transfer by calling `acceptOwnership`. + +Note that **a contract can also be the owner of another one**! This opens the door to using, for example, a [Gnosis Safe](https://safe.global), an [Aragon DAO](https://aragon.org), or a totally custom contract that _you_ create. + +In this way, you can use _composability_ to add additional layers of access control complexity to your contracts. Instead of having a single regular Ethereum account (Externally Owned Account, or EOA) as the owner, you could use a 2-of-3 multisig run by your project leads, for example. Prominent projects in the space, such as [MakerDAO](https://makerdao.com), use systems similar to this one. + +## Role-Based Access Control + +While the simplicity of _ownership_ can be useful for simple systems or quick prototyping, different levels of authorization are often needed. You may want an account to have permission to ban users from a system, but not create new tokens. [_Role-Based Access Control (RBAC)_](https://en.wikipedia.org/wiki/Role-based_access_control) offers flexibility in this regard. + +In essence, we will be defining multiple _roles_, each allowed to perform different sets of actions. An account may have, for example, 'moderator', 'minter' or 'admin' roles, which you will then check for instead of simply using `onlyOwner`. This check can be enforced through the `onlyRole` modifier. Separately, you will be able to define rules for how accounts can be granted a role, have it revoked, and more. + +Most software uses access control systems that are role-based: some users are regular users, some may be supervisors or managers, and a few will often have administrative privileges. + +### Using `AccessControl` + +OpenZeppelin Contracts provides `AccessControl` for implementing role-based access control. Its usage is straightforward: for each role that you want to define, +you will create a new _role identifier_ that is used to grant, revoke, and check if an account has that role. + +Here's a simple example of using `AccessControl` in an [TRC-20 token](/tron-contracts/trc20) to define a 'minter' role, which allows accounts that have it to create new tokens: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {AccessControl} from "@openzeppelin/tron-contracts/access/AccessControl.sol"; +import {TRC20} from "@openzeppelin/tron-contracts/token/TRC20/TRC20.sol"; + +contract AccessControlTRC20MintBase is TRC20, AccessControl { + // Create a new role identifier for the minter role + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + error CallerNotMinter(address caller); + + constructor(address minter) TRC20("MyToken", "TKN") { + // Grant the minter role to a specified account + _grantRole(MINTER_ROLE, minter); + } + + function mint(address to, uint256 amount) public { + // Check that the calling account has the minter role + if (!hasRole(MINTER_ROLE, msg.sender)) { + revert CallerNotMinter(msg.sender); + } + _mint(to, amount); + } +} +``` + + +Make sure you fully understand how `AccessControl` works before using it on your system, or copy-pasting the examples from this guide. + + +While clear and explicit, this isn't anything we wouldn't have been able to achieve with `Ownable`. Indeed, where `AccessControl` shines is in scenarios where granular permissions are required, which can be implemented by defining _multiple_ roles. + +Let's augment our TRC-20 token example by also defining a 'burner' role, which lets accounts destroy tokens, and by using the `onlyRole` modifier: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {AccessControl} from "@openzeppelin/tron-contracts/access/AccessControl.sol"; +import {TRC20} from "@openzeppelin/tron-contracts/token/TRC20/TRC20.sol"; + +contract AccessControlTRC20Mint is TRC20, AccessControl { + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); + + constructor(address minter, address burner) TRC20("MyToken", "TKN") { + _grantRole(MINTER_ROLE, minter); + _grantRole(BURNER_ROLE, burner); + } + + function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { + _mint(to, amount); + } + + function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) { + _burn(from, amount); + } +} +``` + +So clean! By splitting concerns this way, more granular levels of permission may be implemented than were possible with the simpler _ownership_ approach to access control. Limiting what each component of a system is able to do is known as the [principle of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege), and is a good security practice. Note that each account may still have more than one role, if so desired. + +### Granting and Revoking Roles + +The TRC-20 token example above uses `_grantRole`, an `internal` function that is useful when programmatically assigning roles (such as during construction). But what if we later want to grant the 'minter' role to additional accounts? + +By default, **accounts with a role cannot grant it or revoke it from other accounts**: all having a role does is making the `hasRole` check pass. To grant and revoke roles dynamically, you will need help from the _role's admin_. + +Every role has an associated admin role, which grants permission to call the `grantRole` and `revokeRole` functions. A role can be granted or revoked by using these if the calling account has the corresponding admin role. Multiple roles may have the same admin role to make management easier. A role's admin can even be the same role itself, which would cause accounts with that role to be able to also grant and revoke it. + +This mechanism can be used to create complex permissioning structures resembling organizational charts, but it also provides an easy way to manage simpler applications. `AccessControl` includes a special role, called `DEFAULT_ADMIN_ROLE`, which acts as the **default admin role for all roles**. An account with this role will be able to manage any other role, unless `_setRoleAdmin` is used to select a new admin role. + +Since it is the admin for all roles by default, and in fact it is also its own admin, this role carries significant risk. To mitigate this risk we provide `AccessControlDefaultAdminRules`, a recommended extension of `AccessControl` that adds a number of enforced security measures for this role: the admin is restricted to a single account, with a 2-step transfer procedure with a delay between steps. + +Let's take a look at the TRC-20 token example, this time taking advantage of the default admin role: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {AccessControl} from "@openzeppelin/tron-contracts/access/AccessControl.sol"; +import {TRC20} from "@openzeppelin/tron-contracts/token/TRC20/TRC20.sol"; + +contract AccessControlTRC20MintMissing is TRC20, AccessControl { + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); + + constructor() TRC20("MyToken", "TKN") { + // Grant the contract deployer the default admin role: it will be able + // to grant and revoke any roles + _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { + _mint(to, amount); + } + + function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) { + _burn(from, amount); + } +} +``` + +Note that, unlike the previous examples, no accounts are granted the 'minter' or 'burner' roles. However, because those roles' admin role is the default admin role, and _that_ role was granted to `msg.sender`, that same account can call `grantRole` to give minting or burning permission, and `revokeRole` to remove it. + +Dynamic role allocation is often a desirable property, for example in systems where trust in a participant may vary over time. It can also be used to support use cases such as [KYC](https://en.wikipedia.org/wiki/Know_your_customer), where the list of role-bearers may not be known up-front, or may be prohibitively expensive to include in a single transaction. + +### Querying Privileged Accounts + +Because accounts might [grant and revoke roles](#granting-and-revoking-roles) dynamically, it is not always possible to determine which accounts hold a particular role. This is important as it allows proving certain properties about a system, such as that an administrative account is a multisig or a DAO, or that a certain role has been removed from all users, effectively disabling any associated functionality. + +The base `AccessControl` contract provides role-based access control, but it does not support on-chain enumeration of role members. To track which accounts hold a role, you should instead rely on the RoleGranted and RoleRevoked events, which can be processed off-chain. If on-chain enumeration is required, use the AccessControlEnumerable extension. + +This contract uses `EnumerableSet` internally and provides the following functions: + +- `getRoleMemberCount` +- `getRoleMember` +- `getRoleMembers` + +These can be used to iterate over the accounts that have been granted a role: + +```javascript +const minterCount = await myToken.getRoleMemberCount(MINTER_ROLE); + +const members = []; +for (let i = 0; i < minterCount; ++i) { + members.push(await myToken.getRoleMember(MINTER_ROLE, i)); +} +``` + +## Delayed operation + +Access control is essential to prevent unauthorized access to critical functions. These functions may be used to mint tokens, freeze transfers or perform an upgrade that completely changes the smart contract logic. While `Ownable` and `AccessControl` can prevent unauthorized access, they do not address the issue of a misbehaving administrator attacking their own system to the prejudice of their users. + +This is the issue the `TimelockController` is addressing. + +The `TimelockController` is a proxy that is governed by proposers and executors. When set as the owner/admin/controller of a smart contract, it ensures that whichever maintenance operation is ordered by the proposers is subject to a delay. This delay protects the users of the smart contract by giving them time to review the maintenance operation and exit the system if they consider it is in their best interest to do so. + +### Using `TimelockController` + +By default, the address that deployed the `TimelockController` gets administration privileges over the timelock. This role grants the right to assign proposers, executors, and other administrators. + +The first step in configuring the `TimelockController` is to assign at least one proposer and one executor. These can be assigned during construction or later by anyone with the administrator role. These roles are not exclusive, meaning an account can have both roles. + +Roles are managed using the `AccessControl` interface and the `bytes32` values for each role are accessible through the `DEFAULT_ADMIN_ROLE`, `PROPOSER_ROLE`, `EXECUTOR_ROLE`, and `CANCELLER_ROLE` constants. + +There is an additional feature built on top of `AccessControl`: giving the executor role to `address(0)` opens access to anyone to execute a proposal once the timelock has expired. This feature, while useful, should be used with caution. + +At this point, with both a proposer and an executor assigned, the timelock can perform operations. + +An optional next step is for the deployer to renounce its administrative privileges and leave the timelock self-administered. If the deployer decides to do so, all further maintenance, including assigning new proposers/schedulers or changing the timelock duration will have to follow the timelock workflow. This links the governance of the timelock to the governance of contracts attached to the timelock, and enforces a delay on timelock maintenance operations. + + +If the deployer renounces administrative rights in favour of timelock itself, assigning new proposers or executors will require a timelocked operation. This means that if the accounts in charge of any of these two roles become unavailable, then the entire contract (and any contract it controls) becomes locked indefinitely. + + +With both the proposer and executor roles assigned and the timelock in charge of its own administration, you can now transfer the ownership/control of any contract to the timelock. + + +A recommended configuration is to grant both roles to a secure governance contract such as a DAO or a multisig, and to additionally grant the executor role to a few EOAs held by people in charge of helping with the maintenance operations. These wallets cannot take over control of the timelock but they can help smoothen the workflow. + + +### Minimum delay + +Operations executed by the `TimelockController` are not subject to a fixed delay but rather a minimum delay. Some major updates might call for a longer delay. For example, if a delay of just a few days might be sufficient for users to audit a minting operation, it makes sense to use a delay of a few weeks, or even a few months, when scheduling a smart contract upgrade. + +The minimum delay (accessible through the `getMinDelay` method) can be updated by calling the `updateDelay` function. Bear in mind that access to this function is only accessible by the timelock itself, meaning this maintenance operation has to go through the timelock itself. + +## Access Management + +For a system of contracts, better integrated role management can be achieved with an `AccessManager` instance. Instead of managing each contract's permission separately, AccessManager stores all the permissions in a single contract, making your protocol easier to audit and maintain. + +Although `AccessControl` offers a more dynamic solution for adding permissions to your contracts than Ownable, decentralized protocols tend to become more complex after integrating new contract instances and requires you to keep track of permissions separately in each contract. This increases the complexity of permissions management and monitoring across the system. + +![Access Control multiple](/tron/access-control-multiple.svg) + +Protocols managing permissions in production systems often require more integrated alternatives to fragmented permissions through multiple `AccessControl` instances. + +![AccessManager](/tron/access-manager.svg) + +The AccessManager is designed around the concept of role and target functions: + +- Roles are granted to accounts (addresses) following a many-to-many approach for flexibility. This means that each user can have one or multiple roles and multiple users can have the same role. +- Access to a restricted target function is limited to one role. A target function is defined by one [function selector](https://docs.soliditylang.org/en/v0.8.20/abi-spec.html#function-selector) on one contract (called target). + +For a call to be authorized, the caller must bear the role that is assigned to the current target function (contract address + function selector). + +![AccessManager functions](/tron/access-manager-functions.svg) + +### Using `AccessManager` + +OpenZeppelin Contracts provides `AccessManager` for managing roles across any number of contracts. The `AccessManager` itself is a contract that can be deployed and used out of the box. It sets an initial admin in the constructor who will be allowed to perform management operations. + +In order to restrict access to some functions of your contract, you should inherit from the `AccessManaged` contract provided along with the manager. This provides the `restricted` modifier that can be used to protect any externally facing function. Note that you will have to specify the address of the AccessManager instance (`initialAuthority`) in the constructor so the `restricted` modifier knows which manager to use for checking permissions. + +Here's a simple example of an [TRC-20 token](/tron-contracts/tokens#TRC20) that defines a `mint` function that is restricted by an `AccessManager`: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {AccessManaged} from "@openzeppelin/tron-contracts/access/manager/AccessManaged.sol"; +import {TRC20} from "@openzeppelin/tron-contracts/token/TRC20/TRC20.sol"; + +contract AccessManagedTRC20Mint is TRC20, AccessManaged { + constructor(address manager) TRC20("MyToken", "TKN") AccessManaged(manager) {} + + // Minting is restricted according to the manager rules for this function. + // The function is identified by its selector: 0x40c10f19. + // Calculated with bytes4(keccak256('mint(address,uint256)')) + function mint(address to, uint256 amount) public restricted { + _mint(to, amount); + } +} +``` + + +Make sure you fully understand how `AccessManager` works before using it or copy-pasting the examples from this guide. + + +Once the managed contract has been deployed, it is now under the manager's control. The initial admin can then assign the minter role to an address and also allow the role to call the `mint` function. For example, this is demonstrated in the following Javascript code using Ethers.js: + +```javascript +// const target = ...; +// const user = ...; +const MINTER = 42n; // Roles are uint64 (0 is reserved for the ADMIN_ROLE) + +// Grant the minter role with no execution delay +await manager.grantRole(MINTER, user, 0); + +// Allow the minter role to call the function selector +// corresponding to the mint function +await manager.setTargetFunctionRole( + target, + ['0x40c10f19'], // bytes4(keccak256('mint(address,uint256)')) + MINTER +); +``` + +Even though each role has its own list of function permissions, each role member (`address`) has an execution delay that will dictate how long the account should wait to execute a function that requires its role. Delayed operations must have the `schedule` function called on them first in the AccessManager before they can be executed, either by calling the target function or using the AccessManager's `execute` function. + +Additionally, roles can have a granting delay that prevents adding members immediately. The AccessManager admins can set this grant delay as follows: + +```javascript +const HOUR = 60 * 60; + +const GRANT_DELAY = 24 * HOUR; +const EXECUTION_DELAY = 5 * HOUR; +const ACCOUNT = "0x..."; + +await manager.connect(initialAdmin).setGrantDelay(MINTER, GRANT_DELAY); + +// The role will go into effect after the GRANT_DELAY passes +await manager.connect(initialAdmin).grantRole(MINTER, ACCOUNT, EXECUTION_DELAY); +``` + +Note that roles do not define a name. As opposed to the `AccessControl` case, roles are identified as numeric values instead of being hardcoded in the contract as `bytes32` values. It is still possible to allow for tooling discovery (e.g. for role exploration) using role labeling with the `labelRole` function. + +```javascript +await manager.labelRole(MINTER, "MINTER"); +``` + +Given the admins of the `AccessManaged` can modify all of its permissions, it's recommended to keep only a single admin address secured under a multisig or governance layer. To achieve this, it is possible for the initial admin to set up all the required permissions, targets, and functions, assign a new admin, and finally renounce its admin role. + +For improved incident response coordination, the manager includes a mode where administrators can completely close a target contract. When closed, all calls to restricted target functions in a target contract will revert. + +Closing and opening contracts don't alter any of their settings, neither permissions nor delays. Particularly, the roles required for calling specific target functions are not modified. + +This mode is useful for incident response operations that require temporarily shutting down a contract in order to evaluate emergencies and reconfigure permissions. + +```javascript +const target = await myToken.getAddress(); + +// Token's `restricted` functions closed +await manager.setTargetClosed(target, true); + +// Token's `restricted` functions open +await manager.setTargetClosed(target, false); +``` + + +Even if an `AccessManager` defines permissions for a target function, these won't be applied if the managed contract instance is not using the `restricted` modifier for that function, or if its manager is a different one. + + +### Role Admins and Guardians + +An important aspect of the AccessControl contract is that roles aren't granted nor revoked by role members. Instead, it relies on the concept of a role admin for granting and revoking. + +In the case of the `AccessManager`, the same rule applies and only the role's admins are able to call grant and revoke functions. Note that calling these functions will be subject to the execution delay that the executing role admin has. + +Additionally, the `AccessManager` stores a _guardian_ as an extra protection for each role. This guardian has the ability to cancel operations that have been scheduled by any role member with an execution delay. Consider that a role will have its initial admin and guardian default to the `ADMIN_ROLE` (`0`). + + +Be careful with the members of `ADMIN_ROLE`, since it acts as the default admin and guardian for every role. A misbehaved guardian can cancel operations at will, affecting the AccessManager's operation. + + +### Manager configuration + +The `AccessManager` provides a built-in interface for configuring permission settings that can be accessed by its `ADMIN_ROLE` members. + +This configuration interface includes the following functions: + +- Add a label to a role using the `labelRole` function. +- Assign the admin and guardian of a role with `setRoleAdmin` and `setRoleGuardian`. +- Set each role's grant delay via `setGrantDelay`. + +As an admin, some actions will require a delay. Similar to each member's execution delay, some admin operations require waiting for execution and should follow the `schedule` and `execute` workflow. + +More specifically, these delayed functions are those for configuring the settings of a specific target contract. The delay applied to these functions can be adjusted by the manager admins with `setTargetAdminDelay`. + +The delayed admin actions are: + +- Updating an `AccessManaged` contract authority using `updateAuthority`. +- Closing or opening a target via `setTargetClosed`. +- Changing permissions of whether a role can call a target function with `setTargetFunctionRole`. + +### Manager Enumerability + +Similar to `AccessControl`, accounts might be granted and revoked roles dynamically in an `AccessManager`, making it challenging to determine which accounts hold a particular role at any given time. This capability is essential for proving certain properties about a system, such as verifying that an administrative role is held by a multisig or DAO, or that a certain role has been completely removed to disable associated functionality. + +The base `AccessManager` contract provides comprehensive role-based access control but does not support on-chain enumeration of role members or target function permissions by default. To track which accounts hold roles and which functions are assigned to roles, you should rely on the RoleGranted, RoleRevoked, and TargetFunctionRoleUpdated events, which can be processed off-chain. + +If on-chain enumeration is required, it can be added implemented on top of the existing logic: + +```solidity +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import {AccessManager} from "@openzeppelin/tron-contracts/access/manager/AccessManager.sol"; +import {EnumerableSet} from "@openzeppelin/tron-contracts/utils/structs/EnumerableSet.sol"; + +/** + * @dev Extension of {AccessManager} that allows enumerating the members of each role + * and the target functions each role is allowed to call. + * + * NOTE: Given {ADMIN_ROLE} is the default role for every restricted function, the + * {getRoleTargetFunctions} and {getRoleTargetFunctionCount} functions will return an empty array + * and 0 respectively. + */ +abstract contract AccessManagerEnumerable is AccessManager { + using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableSet for EnumerableSet.Bytes4Set; + + mapping(uint64 roleId => EnumerableSet.AddressSet) private _roleMembers; + mapping(uint64 roleId => mapping(address target => EnumerableSet.Bytes4Set)) private _roleTargetFunctions; + + /** + * @dev Returns the number of accounts that have `roleId`. Can be used + * together with {getRoleMember} to enumerate all bearers of a role. + */ + function getRoleMemberCount(uint64 roleId) public view virtual returns (uint256) { + return _roleMembers[roleId].length(); + } + + /** + * @dev Returns one of the accounts that have `roleId`. `index` must be a + * value between 0 and {getRoleMemberCount}, non-inclusive. + * + * Role bearers are not sorted in any particular way, and their ordering may change at any point. + * + * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure + * you perform all queries on the same block. See the following + * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] + * for more information. + */ + function getRoleMember(uint64 roleId, uint256 index) public view virtual returns (address) { + return _roleMembers[roleId].at(index); + } + + /** + * @dev Returns a range of accounts that have `roleId`. `start` and `end` define the range bounds. + * `start` is inclusive and `end` is exclusive. + * + * Role bearers are not sorted in any particular way, and their ordering may change at any point. + * + * It is not necessary to call {getRoleMemberCount} before calling this function. Using `start = 0` and + * `end = type(uint256).max` will return every member of `roleId`. + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function getRoleMembers(uint64 roleId, uint256 start, uint256 end) public view virtual returns (address[] memory) { + return _roleMembers[roleId].values(start, end); + } + + /** + * @dev Returns the number of target function selectors that require `roleId` for the given `target`. + * Can be used together with {getRoleTargetFunction} to enumerate all target functions for a role on a specific target. + * + * NOTE: Given {ADMIN_ROLE} is the default role for every restricted function, passing {ADMIN_ROLE} as `roleId` will + * return 0. See {_updateRoleTargetFunction} for more details. + */ + function getRoleTargetFunctionCount(uint64 roleId, address target) public view virtual returns (uint256) { + return _roleTargetFunctions[roleId][target].length(); + } + + /** + * @dev Returns one of the target function selectors that require `roleId` for the given `target`. + * `index` must be a value between 0 and {getRoleTargetFunctionCount}, non-inclusive. + * + * Target function selectors are not sorted in any particular way, and their ordering may change at any point. + * + * WARNING: When using {getRoleTargetFunction} and {getRoleTargetFunctionCount}, make sure + * you perform all queries on the same block. See the following + * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] + * for more information. + */ + function getRoleTargetFunction(uint64 roleId, address target, uint256 index) public view virtual returns (bytes4) { + return _roleTargetFunctions[roleId][target].at(index); + } + + /** + * @dev Returns a range of target function selectors that require `roleId` for the given `target`. + * `start` and `end` define the range bounds. `start` is inclusive and `end` is exclusive. + * + * Target function selectors are not sorted in any particular way, and their ordering may change at any point. + * + * It is not necessary to call {getRoleTargetFunctionCount} before calling this function. Using `start = 0` and + * `end = type(uint256).max` will return every function selector that `roleId` is allowed to call on `target`. + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + * + * NOTE: Given {ADMIN_ROLE} is the default role for every restricted function, passing {ADMIN_ROLE} as `roleId` will + * return an empty array. See {_updateRoleTargetFunction} for more details. + */ + function getRoleTargetFunctions( + uint64 roleId, + address target, + uint256 start, + uint256 end + ) public view virtual returns (bytes4[] memory) { + return _roleTargetFunctions[roleId][target].values(start, end); + } + + /// @dev See {AccessManager-_grantRole}. Adds the account to the role members set. + function _grantRole( + uint64 roleId, + address account, + uint32 grantDelay, + uint32 executionDelay + ) internal virtual override returns (bool) { + bool granted = super._grantRole(roleId, account, grantDelay, executionDelay); + if (granted) { + _roleMembers[roleId].add(account); + } + return granted; + } + + /// @dev See {AccessManager-_revokeRole}. Removes the account from the role members set. + function _revokeRole(uint64 roleId, address account) internal virtual override returns (bool) { + bool revoked = super._revokeRole(roleId, account); + if (revoked) { + _roleMembers[roleId].remove(account); + } + return revoked; + } + + /** + * @dev See {AccessManager-_setTargetFunctionRole}. Adds the selector to the role target functions set. + * + * NOTE: This function does not track function selectors for the {ADMIN_ROLE}, since exhaustively tracking + * all restricted/admin functions is impractical (by default, all restricted functions are assigned to {ADMIN_ROLE}). + * Therefore, roles assigned as {ADMIN_ROLE} will not have their selectors included in this extension's tracking. + */ + function _setTargetFunctionRole(address target, bytes4 selector, uint64 roleId) internal virtual override { + // cache old role ID + uint64 oldRoleId = getTargetFunctionRole(target, selector); + + // call super + super._setTargetFunctionRole(target, selector, roleId); + + // update enumerable sets + if (oldRoleId != ADMIN_ROLE) { + _roleTargetFunctions[oldRoleId][target].remove(selector); + } + if (roleId != ADMIN_ROLE) { + _roleTargetFunctions[roleId][target].add(selector); + } + } +} +``` + + +The enumerable example only enumerates members of a role and functions that each role can call. Yet, it's possible to enumerate roles active (i.e. roles granted to at least 1 member), guardians and admins. + + +This adds function that can be queried to iterate over the accounts that have been granted a role and the functions that a role is allowed to call on specific targets: + +```javascript +// Enumerate role members +const minterCount = await accessManager.getRoleMemberCount(MINTER_ROLE); + +const members = []; +for (let i = 0; i < minterCount; ++i) { + members.push(await accessManager.getRoleMember(MINTER_ROLE, i)); +} + +// Or get all members at once +const allMembers = await accessManager.getRoleMembers(MINTER_ROLE, 0, ethers.MaxUint256); + +// Enumerate target functions for a role +const target = await myToken.getAddress(); +const functionCount = await accessManager.getRoleTargetFunctionCount(MINTER_ROLE, target); + +const functions = []; +for (let i = 0; i < functionCount; ++i) { + functions.push(await accessManager.getRoleTargetFunction(MINTER_ROLE, target, i)); +} + +// Or get all functions at once +const allFunctions = await accessManager.getRoleTargetFunctions(MINTER_ROLE, target, 0, ethers.MaxUint256); +``` + +### Using with Ownable + +Contracts already inheriting from `Ownable` can migrate to AccessManager by transferring ownership to the manager. After that, all calls to functions with the `onlyOwner` modifier should be called through the manager's `execute` function, even if the caller doesn't require a delay. + +```javascript +await ownable.connect(owner).transferOwnership(accessManager); +``` + +### Using with AccessControl + +For systems already using `AccessControl`, the `DEFAULT_ADMIN_ROLE` can be granted to the `AccessManager` after revoking every other role. Subsequent calls should be made through the manager's `execute` method, similar to the Ownable case. + +```javascript +// Revoke old roles +await accessControl.connect(admin).revokeRole(MINTER_ROLE, account); + +// Grant the admin role to the access manager +await accessControl.connect(admin).grantRole(DEFAULT_ADMIN_ROLE, accessManager); + +await accessControl.connect(admin).renounceRole(DEFAULT_ADMIN_ROLE, admin); +``` + + +After migrating to AccessManager, the `msg.sender` in restricted functions will be the AccessManager contract itself through the `execute` function, not the original caller. This is a fundamental change in how access control works and may require updates to your contract logic or frontend integration. + diff --git a/content/tron-contracts/backwards-compatibility.mdx b/content/tron-contracts/backwards-compatibility.mdx new file mode 100644 index 00000000..58804f31 --- /dev/null +++ b/content/tron-contracts/backwards-compatibility.mdx @@ -0,0 +1,51 @@ +--- +title: "Backwards Compatibility" +--- + +OpenZeppelin Contracts uses semantic versioning to communicate backwards compatibility of its API and storage layout. Patch and minor updates will generally be backwards compatible, with rare exceptions as detailed below. Major updates should be assumed incompatible with previous releases. On this page, we provide details about these guarantees. + +## API + +In backwards compatible releases, all changes should be either additions or modifications to internal implementation details. Most code should continue to compile and behave as expected. The exceptions to this rule are listed below. + +### Security + +Infrequently a patch or minor update will remove or change an API in a breaking way, but only if the previous API is considered insecure. These breaking changes will be noted in the changelog and release notes, and published along with a security advisory. + +### Draft or Pre-Final ERCs + +ERCs that are not Final can change in incompatible ways. For this reason, we avoid shipping implementations of ERCs before they are Final. Some exceptions are made for ERCs that have been published for a long time and seem unlikely to change. Implementations for ERCs that may have breaking changes are published in files named `draft-*.sol` to make that condition explicit. There is no backwards compatibility guarantee for content in files prefixed with `draft`. + +Standards that have achieved widespread adoption with strong backwards compatibility expectations from the community may be treated as de-facto finalized and published without the `draft-` prefix, as extensive ecosystem reliance makes breaking changes highly unlikely. + +### Virtual & Overrides + +Almost all functions in this library are virtual with some exceptions, but this does not mean that overrides are encouraged. There is a subset of functions that are designed to be overridden. By defining overrides outside of this subset you are potentially relying on internal implementation details. We make efforts to preserve backwards compatibility even in these cases but it is extremely difficult and easy to accidentally break. Caution is advised. + +Additionally, some minor updates may result in new compilation errors of the kind "two or more base classes define function with same name and parameter types" or "need to specify overridden contract", due to what Solidity considers ambiguity in inherited functions. This should be resolved by adding an override that invokes the function via `super`. + +See [Extending Contracts](/tron-contracts/extending-contracts) for more about virtual and overrides. + +### Structs + +Struct members with an underscore prefix should be considered "private" and may break in minor versions. Struct data should only be accessed and modified through library functions. + +### Errors + +The specific error format and data that is included with reverts should not be assumed stable unless otherwise specified. + +### Major Releases + +Major releases should be assumed incompatible. Nevertheless, the external interfaces of contracts will remain compatible if they are standardized, or if the maintainers judge that changing them would cause significant strain on the ecosystem. + +An important aspect that major releases may break is "upgrade compatibility", in particular storage layout compatibility. It will never be safe for a live contract to upgrade from one major release to another. + +## Storage Layout + +Minor and patch updates always preserve storage layout compatibility. This means that a live contract can be upgraded from one minor to another without corrupting the storage layout. In some cases it may be necessary to initialize new state variables when upgrading, although we expect this to be infrequent. + +We recommend using OpenZeppelin Upgrades Plugins or CLI to ensure storage layout safety of upgrades. + +## Solidity Version + +The minimum Solidity version required to compile the contracts will remain unchanged in minor and patch updates. New contracts introduced in minor releases may make use of newer Solidity features and require a more recent version of the compiler. diff --git a/content/tron-contracts/extending-contracts.mdx b/content/tron-contracts/extending-contracts.mdx new file mode 100644 index 00000000..7f946fff --- /dev/null +++ b/content/tron-contracts/extending-contracts.mdx @@ -0,0 +1,84 @@ +--- +title: "Extending Contracts" +--- + +Most of the OpenZeppelin Contracts are expected to be used via [inheritance](https://solidity.readthedocs.io/en/latest/contracts.html#inheritance): you will _inherit_ from them when writing your own contracts. + +This is the commonly found `is` syntax, like in `contract MyToken is TRC20`. + + +Unlike ``contract``s, Solidity ``library``s are not inherited from and instead rely on the [`using for`](https://solidity.readthedocs.io/en/latest/contracts.html#using-for) syntax. + +OpenZeppelin Contracts has some ``library``s: most are in the Utils directory. + + +## Overriding + +Inheritance is often used to add the parent contract's functionality to your own contract, but that's not all it can do. You can also _change_ how some parts of the parent behave using _overrides_. + +For example, imagine you want to change `AccessControl` so that `revokeRole` can no longer be called. This can be achieved using overrides: + +```solidity +// contracts/AccessControlModified.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {AccessControl} from "@openzeppelin/tron-contracts/access/AccessControl.sol"; + +contract AccessControlModified is AccessControl { + error AccessControlNonRevocable(); + + // Override the revokeRole function + function revokeRole(bytes32, address) public pure override { + revert AccessControlNonRevocable(); + } +} +``` + +The old `revokeRole` is then replaced by our override, and any calls to it will immediately revert. We cannot _remove_ the function from the contract, but reverting on all calls is good enough. + +### Calling `super` + +Sometimes you want to _extend_ a parent's behavior, instead of outright changing it to something else. This is where `super` comes in. + +The `super` keyword will let you call functions defined in a parent contract, even if they are overridden. This mechanism can be used to add additional checks to a function, emit events, or otherwise add functionality as you see fit. + + +For more information on how overrides work, head over to the [official Solidity documentation](https://solidity.readthedocs.io/en/latest/contracts.html#index-17). + + +Here is a modified version of `AccessControl` where `revokeRole` cannot be used to revoke the `DEFAULT_ADMIN_ROLE`: + +```solidity +// contracts/AccessControlNonRevokableAdmin.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {AccessControl} from "@openzeppelin/tron-contracts/access/AccessControl.sol"; + +contract AccessControlNonRevokableAdmin is AccessControl { + error AccessControlNonRevokable(); + + function revokeRole(bytes32 role, address account) public override { + if (role == DEFAULT_ADMIN_ROLE) { + revert AccessControlNonRevokable(); + } + + super.revokeRole(role, account); + } +} +``` + +The `super.revokeRole` statement at the end will invoke ``AccessControl``'s original version of `revokeRole`, the same code that would've run if there were no overrides in place. + + +The same rule is implemented and extended in `AccessControlDefaultAdminRules`, an extension that also adds enforced security measures for the `DEFAULT_ADMIN_ROLE`. + + +## Security + +The maintainers of OpenZeppelin Contracts are mainly concerned with the correctness and security of the code as published in the library, and the combinations of base contracts with the official extensions from the library. + +Custom overrides, especially to hooks, can disrupt important assumptions and may introduce security risks in the code that was previously secure. While we try to ensure the contracts remain secure in the face of a wide range of potential customizations, this is done in a best-effort manner. While we try to document all important assumptions, this should not be relied upon. Custom overrides should be carefully reviewed and checked against the source code of the contract they are customizing to fully understand their impact and guarantee their security. + +The way functions interact internally should not be assumed to stay stable across releases of the library. For example, a function that is used in one context in a particular release may not be used in the same context in the next release. Contracts that override functions should revalidate their assumptions when updating the version of OpenZeppelin Contracts they are built on. diff --git a/content/tron-contracts/faq.mdx b/content/tron-contracts/faq.mdx new file mode 100644 index 00000000..208e1586 --- /dev/null +++ b/content/tron-contracts/faq.mdx @@ -0,0 +1,15 @@ +--- +title: "Frequently Asked Questions" +--- + +## Can I restrict a function to EOAs only? + +When calling external addresses from your contract it is unsafe to assume that an address is an externally-owned account (EOA) and not a contract. Attempting to prevent calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract constructor. + +Although checking that the address has code, `address.code.length > 0`, may seem to differentiate contracts from EOAs, it can only say that an address is currently a contract, and its negation (that an address is not currently a contract) does not imply that the address is an EOA. Some counterexamples are: + +- address of a contract in construction +- address where a contract will be created +- address where a contract lived, but was destroyed + +Furthermore, an address will be considered a contract within the same transaction where it is scheduled for destruction by `SELFDESTRUCT`, which only has an effect at the end of the entire transaction. diff --git a/content/tron-contracts/governance.mdx b/content/tron-contracts/governance.mdx new file mode 100644 index 00000000..443e499f --- /dev/null +++ b/content/tron-contracts/governance.mdx @@ -0,0 +1,411 @@ +--- +title: "How to set up on-chain governance" +--- + +In this guide we will learn how OpenZeppelin's Governor contract works, how to set it up, and how to use it to create proposals, vote for them, and execute them, using tools provided by Ethers.js and Tally. + + +Find detailed contract documentation at Governance API. + + +## Introduction + +Decentralized protocols are in constant evolution from the moment they are publicly released. Often, the initial team retains control of this evolution in the first stages, but eventually delegates it to a community of stakeholders. The process by which this community makes decisions is called on-chain governance, and it has become a central component of decentralized protocols, fueling varied decisions such as parameter tweaking, smart contract upgrades, integrations with other protocols, treasury management, grants, etc. + +This governance protocol is generally implemented in a special-purpose contract called "Governor". The GovernorAlpha and GovernorBravo contracts designed by Compound have been very successful and popular so far, with the downside that projects with different requirements have had to fork the code to customize it for their needs, which can pose a high risk of introducing security issues. For OpenZeppelin Contracts, we set out to build a modular system of Governor contracts so that forking is not needed, and different requirements can be accommodated by writing small modules using Solidity inheritance. You will find the most common requirements out of the box in OpenZeppelin Contracts, but writing additional ones is simple, and we will be adding new features as requested by the community in future releases. Additionally, the design of OpenZeppelin Governor requires minimal use of storage and results in more gas efficient operation. + +## Compatibility + +OpenZeppelin's Governor system was designed with a concern for compatibility with existing systems that were based on Compound's GovernorAlpha and GovernorBravo. Because of this, you will find that many modules are presented in two variants, one of which is built for compatibility with those systems. + +### TRC20Votes & TRC20VotesComp + +The TRC-20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the "Comp" variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only. + +### Governor & GovernorStorage + +An OpenZeppelin Governor contract is not interface-compatible with Compound's GovernorAlpha or Bravo. Even though events are fully compatible, proposal lifecycle functions (creation, execution, etc.) have different signatures that are meant to optimize storage use. Other functions from GovernorAlpha and Bravo are likewise not available. It's possible to opt in some Bravo-like behavior by inheriting from the GovernorStorage module. This module provides proposal enumerability and alternate versions of the `queue`, `execute` and `cancel` function that only take the proposal id. This module reduces the calldata needed by some operations in exchange for an increased storage footprint. This might be a good trade-off for some L2 chains. It also provides primitives for indexer-free frontends. + +Note that even with the use of this module, one important difference with Compound's GovernorBravo is the way that `proposalId`s are calculated. Governor uses the hash of the proposal parameters with the purpose of keeping its data off-chain by event indexing, while the original Bravo implementation uses sequential `proposalId`s. + +### GovernorTimelockControl & GovernorTimelockCompound + +When using a timelock with your Governor contract, you can use either OpenZeppelin's TimelockController or Compound's Timelock. Based on the choice of timelock, you should choose the corresponding Governor module: GovernorTimelockControl or GovernorTimelockCompound respectively. This allows you to migrate an existing GovernorAlpha instance to an OpenZeppelin-based Governor without changing the timelock in use. + +### Tally + +[Tally](https://www.tally.xyz) is a full-fledged application for user owned on-chain governance. It comprises a voting dashboard, proposal creation wizard, real time research and analysis, and educational content. + +For all of these options, the Governor will be compatible with Tally: users will be able to create proposals, see voting periods and delays following ITRC6372, visualize voting power and advocates, navigate proposals, and cast votes. For proposal creation in particular, projects can also use [Defender Transaction Proposals](https://docs.openzeppelin.com/defender/module/actions#transaction-proposals-reference) as an alternative interface. + +In the rest of this guide, we will focus on a fresh deploy of the vanilla OpenZeppelin Governor features without concern for compatibility with GovernorAlpha or Bravo. + +## Setup + +### Token + +The voting power of each account in our governance setup will be determined by a TRC-20 token. The token has to implement the TRC20Votes extension. This extension will keep track of historical balances so that voting power is retrieved from past snapshots rather than current balance, which is an important protection that prevents double voting. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {TRC20} from "@openzeppelin/tron-contracts/token/TRC20/TRC20.sol"; +import {TRC20Permit} from "@openzeppelin/tron-contracts/token/TRC20/extensions/TRC20Permit.sol"; +import {TRC20Votes} from "@openzeppelin/tron-contracts/token/TRC20/extensions/TRC20Votes.sol"; +import {Nonces} from "@openzeppelin/tron-contracts/utils/Nonces.sol"; + +contract MyToken is TRC20, TRC20Permit, TRC20Votes { + constructor() TRC20("MyToken", "MTK") TRC20Permit("MyToken") {} + + // The functions below are overrides required by Solidity. + + function _update(address from, address to, uint256 amount) internal override(TRC20, TRC20Votes) { + super._update(from, to, amount); + } + + function nonces(address owner) public view virtual override(TRC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } +} +``` + +If your project already has a live token that does not include TRC20Votes and is not upgradeable, you can wrap it in a governance token by using TRC20Wrapper. This will allow token holders to participate in governance by wrapping their tokens 1-to-1. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ITRC20, TRC20} from "@openzeppelin/tron-contracts/token/TRC20/TRC20.sol"; +import {TRC20Permit} from "@openzeppelin/tron-contracts/token/TRC20/extensions/TRC20Permit.sol"; +import {TRC20Votes} from "@openzeppelin/tron-contracts/token/TRC20/extensions/TRC20Votes.sol"; +import {TRC20Wrapper} from "@openzeppelin/tron-contracts/token/TRC20/extensions/TRC20Wrapper.sol"; +import {Nonces} from "@openzeppelin/tron-contracts/utils/Nonces.sol"; + +contract MyTokenWrapped is TRC20, TRC20Permit, TRC20Votes, TRC20Wrapper { + constructor( + ITRC20 wrappedToken + ) TRC20("MyTokenWrapped", "MTK") TRC20Permit("MyTokenWrapped") TRC20Wrapper(wrappedToken) {} + + // The functions below are overrides required by Solidity. + + function decimals() public view override(TRC20, TRC20Wrapper) returns (uint8) { + return super.decimals(); + } + + function _update(address from, address to, uint256 amount) internal override(TRC20, TRC20Votes) { + super._update(from, to, amount); + } + + function nonces(address owner) public view virtual override(TRC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } +} +``` + + +The only other source of voting power available in OpenZeppelin Contracts currently is `TRC721Votes`. TRC-721 tokens that don't provide this functionality can be wrapped into a voting tokens using a combination of `TRC721Votes` and `TRC721Wrapper`. + + + +The internal clock used by the token to store voting balances will dictate the operating mode of the Governor contract attached to it. By default, block numbers are used. Since v4.9, developers can override the ITRC6372 clock to use timestamps instead of block numbers. + + +### Governor + +Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, 3) what options people have when casting a vote and how those votes are counted, and 4) what type of token should be used to vote. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. + +For 1) we will use the GovernorVotes module, which hooks to an IVotes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. This module also discovers the clock mode (ERC-6372) used by the token and applies it to the Governor. + +For 2) we will use GovernorVotesQuorumFraction which works together with TRC20Votes to define quorum as a percentage of the total supply at the block a proposal's voting power is retrieved. This requires a constructor parameter to set the percentage. Most Governors nowadays use 4%, so we will initialize the module with parameter 4 (this indicates the percentage, resulting in 4%). + +For 3) we will use GovernorCountingSimple, a module that offers 3 options to voters: For, Against, and Abstain, and where only For and Abstain votes are counted towards quorum. + +Besides these modules, Governor itself has some parameters we must set. + +votingDelay: How long after a proposal is created should voting power be fixed. A large voting delay gives users time to unstake tokens if necessary. + +votingPeriod: How long does a proposal remain open to votes. + +These parameters are specified in the unit defined in the token's clock. Assuming the token uses block numbers, and assuming block time of around 12 seconds, we will have set votingDelay = 1 day = 7200 blocks, and votingPeriod = 1 week = 50400 blocks. + +We can optionally set a proposal threshold as well. This restricts proposal creation to accounts that have enough voting power. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Governor} from "@openzeppelin/tron-contracts/governance/Governor.sol"; +import {GovernorCountingSimple} from "@openzeppelin/tron-contracts/governance/extensions/GovernorCountingSimple.sol"; +import {GovernorVotes} from "@openzeppelin/tron-contracts/governance/extensions/GovernorVotes.sol"; +import {GovernorVotesQuorumFraction} from "@openzeppelin/tron-contracts/governance/extensions/GovernorVotesQuorumFraction.sol"; +import {GovernorTimelockControl} from "@openzeppelin/tron-contracts/governance/extensions/GovernorTimelockControl.sol"; +import {TimelockController} from "@openzeppelin/tron-contracts/governance/TimelockController.sol"; +import {IVotes} from "@openzeppelin/tron-contracts/governance/utils/IVotes.sol"; + +contract MyGovernor is + Governor, + GovernorCountingSimple, + GovernorVotes, + GovernorVotesQuorumFraction, + GovernorTimelockControl +{ + constructor( + IVotes _token, + TimelockController _timelock + ) Governor("MyGovernor") GovernorVotes(_token) GovernorVotesQuorumFraction(4) GovernorTimelockControl(_timelock) {} + + function votingDelay() public pure override returns (uint256) { + return 7200; // 1 day + } + + function votingPeriod() public pure override returns (uint256) { + return 50400; // 1 week + } + + function proposalThreshold() public pure override returns (uint256) { + return 0; + } + + // The functions below are overrides required by Solidity. + + function state(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (ProposalState) { + return super.state(proposalId); + } + + function proposalNeedsQueuing( + uint256 proposalId + ) public view virtual override(Governor, GovernorTimelockControl) returns (bool) { + return super.proposalNeedsQueuing(proposalId); + } + + function _queueOperations( + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal override(Governor, GovernorTimelockControl) returns (uint48) { + return super._queueOperations(proposalId, targets, values, calldatas, descriptionHash); + } + + function _executeOperations( + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal override(Governor, GovernorTimelockControl) { + super._executeOperations(proposalId, targets, values, calldatas, descriptionHash); + } + + function _cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal override(Governor, GovernorTimelockControl) returns (uint256) { + return super._cancel(targets, values, calldatas, descriptionHash); + } + + function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) { + return super._executor(); + } +} +``` + +### Timelock + +It is good practice to add a timelock to governance decisions. This allows users to exit the system if they disagree with a decision before it is executed. We will use OpenZeppelin's TimelockController in combination with the GovernorTimelockControl module. + + +When using a timelock, it is the timelock that will execute proposals and thus the timelock that should hold any funds, ownership, and access control roles. Before version 4.5 there was no way to recover funds in the Governor contract when using a timelock! Before version 4.3, when using the Compound Timelock, ETH in the timelock was not easily accessible. + + +TimelockController uses an AccessControl setup that we need to understand in order to set up roles. + +- The Proposer role is in charge of queueing operations: this is the role the Governor instance should be granted, and it should likely be the only proposer in the system. +- The Executor role is in charge of executing already available operations: we can assign this role to the special zero address to allow anyone to execute (if operations can be particularly time sensitive, the Governor should be made Executor instead). +- The Canceller role can cancel queued operations in the timelock: the Governor should hold this role for proposal cancellations to work after queuing. If the Governor is set as a proposer at deployment, it will automatically receive the Canceller role as well. +- Lastly, there is the Admin role, which can grant and revoke the two previous roles: this is a very sensitive role that will be granted automatically to the timelock itself, and optionally to a second account, which can be used for ease of setup but should promptly renounce the role. + + +Granting additional proposers or cancellers besides the Governor is risky. They can execute operations as the timelock (bypassing governance) and cancel approved proposals, potentially causing a denial of service attack against governance. + + +## Proposal Lifecycle + +Let's walk through how to create and execute a proposal on our newly deployed Governor. + +A proposal is a sequence of actions that the Governor contract will perform if it passes. Each action consists of a target address, calldata encoding a function call, and an amount of ETH to include. Additionally, a proposal includes a human-readable description. + +### Create a Proposal + +Let's say we want to create a proposal to give a team a grant, in the form of TRC-20 tokens from the governance treasury. This proposal will consist of a single action where the target is the TRC-20 token, calldata is the encoded function call `transfer(, )`, and with 0 ETH attached. + +Generally a proposal will be created with the help of an interface such as Tally or [Defender Proposals](https://docs.openzeppelin.com/defender/module/actions#transaction-proposals-reference). Here we will show how to create the proposal using Ethers.js. + +First we get all the parameters necessary for the proposal action. + +```javascript +const tokenAddress = ...; +const token = await ethers.getContractAt('TRC20', tokenAddress); + +const teamAddress = ...; +const grantAmount = ...; +const transferCalldata = token.interface.encodeFunctionData('transfer', [teamAddress, grantAmount]); +``` + +Now we are ready to call the propose function of the Governor. Note that we don't pass in one array of actions, but instead three arrays corresponding to the list of targets, the list of values, and the list of calldatas. In this case it's a single action, so it's simple: + +```javascript +await governor.propose( + [tokenAddress], + [0], + [transferCalldata], + "Proposal #1: Give grant to team", +); +``` + +This will create a new proposal, with a proposal id that is obtained by hashing together the proposal data, and which will also be found in an event in the logs of the transaction. + +### Cast a Vote + +Once a proposal is active, delegates can cast their vote. Note that it is delegates who carry voting power: if a token holder wants to participate, they can set a trusted representative as their delegate, or they can become a delegate themselves by self-delegating their voting power. + +Votes are cast by interacting with the Governor contract through the `castVote` family of functions. Voters would generally invoke this from a governance UI such as Tally. + +![Voting in Tally](/tron/tally-vote.png) + +### Execute the Proposal + +Once the voting period is over, if quorum was reached (enough voting power participated) and the majority voted in favor, the proposal is considered successful and can proceed to be executed. Once a proposal passes, it can be queued and executed from the same place you voted. + +![Administration Panel in Tally](/tron/tally-exec.png) + +We will see now how to do this manually using Ethers.js. + +If a timelock was set up, the first step to execution is queueing. You will notice that both the queue and execute functions require passing in the entire proposal parameters, as opposed to just the proposal id. This is necessary because this data is not stored on chain, as a measure to save gas. Note that these parameters can always be found in the events emitted by the contract. The only parameter that is not sent in its entirety is the description, since this is only needed in its hashed form to compute the proposal id. + +To queue, we call the queue function: + +```javascript +const descriptionHash = ethers.utils.id("Proposal #1: Give grant to team"); + +await governor.queue( + [tokenAddress], + [0], + [transferCalldata], + descriptionHash, +); +``` + +This will cause the Governor to interact with the timelock contract and queue the actions for execution after the required delay. + +After enough time has passed (according to the timelock parameters), the proposal can be executed. If there was no timelock to begin with, this step can be run immediately after the proposal succeeds. + +```javascript +await governor.execute( + [tokenAddress], + [0], + [transferCalldata], + descriptionHash, +); +``` + +Executing the proposal will transfer the TRC-20 tokens to the chosen recipient. To wrap up: we set up a system where a treasury is controlled by the collective decision of the token holders of a project, and all actions are executed via proposals enforced by on-chain votes. + +## Timestamp based governance + +### Motivation + +It is sometimes difficult to deal with durations expressed in number of blocks because of inconsistent or unpredictable time between blocks. This is particularly true of some L2 networks where blocks are produced based on blockchain usage. Using number of blocks can also lead to the governance rules being affected by network upgrades that modify the expected time between blocks. + +The difficulty of replacing block numbers with timestamps is that the Governor and the token must both use the same format when querying past votes. If a token is designed around block numbers, it is not possible for a Governor to reliably do timestamp based lookups. + +Therefore, designing a timestamp based voting system starts with the token. + +### Token + +Since v4.9, all voting contracts (including `TRC20Votes` and `TRC721Votes`) rely on ITRC6372 for clock management. In order to change from operating with block numbers to operating with timestamps, all that is required is to override the `clock()` and `CLOCK_MODE()` functions. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {TRC20} from "@openzeppelin/tron-contracts/token/TRC20/TRC20.sol"; +import {TRC20Permit} from "@openzeppelin/tron-contracts/token/TRC20/extensions/TRC20Permit.sol"; +import {TRC20Votes} from "@openzeppelin/tron-contracts/token/TRC20/extensions/TRC20Votes.sol"; +import {Nonces} from "@openzeppelin/tron-contracts/utils/Nonces.sol"; + +contract MyTokenTimestampBased is TRC20, TRC20Permit, TRC20Votes { + constructor() TRC20("MyTokenTimestampBased", "MTK") TRC20Permit("MyTokenTimestampBased") {} + + // Overrides ITRC6372 functions to make the token & governor timestamp-based + + function clock() public view override returns (uint48) { + return uint48(block.timestamp); + } + + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public pure override returns (string memory) { + return "mode=timestamp"; + } + + // The functions below are overrides required by Solidity. + + function _update(address from, address to, uint256 amount) internal override(TRC20, TRC20Votes) { + super._update(from, to, amount); + } + + function nonces(address owner) public view virtual override(TRC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } +} +``` + +### Governor + +The Governor will automatically detect the clock mode used by the token and adapt to it. There is no need to override anything in the Governor contract. However, the clock mode does affect how some values are interpreted. It is therefore necessary to set the `votingDelay()` and `votingPeriod()` accordingly. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Governor} from "@openzeppelin/tron-contracts/governance/Governor.sol"; +import {GovernorCountingSimple} from "@openzeppelin/tron-contracts/governance/extensions/GovernorCountingSimple.sol"; +import {GovernorVotes} from "@openzeppelin/tron-contracts/governance/extensions/GovernorVotes.sol"; +import {GovernorVotesQuorumFraction} from "@openzeppelin/tron-contracts/governance/extensions/GovernorVotesQuorumFraction.sol"; +import {GovernorTimelockControl} from "@openzeppelin/tron-contracts/governance/extensions/GovernorTimelockControl.sol"; +import {TimelockController} from "@openzeppelin/tron-contracts/governance/TimelockController.sol"; +import {IVotes} from "@openzeppelin/tron-contracts/governance/utils/IVotes.sol"; + +contract MyGovernor is Governor, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl { + constructor(IVotes _token, TimelockController _timelock) + Governor("MyGovernor") + GovernorVotes(_token) + GovernorVotesQuorumFraction(4) + GovernorTimelockControl(_timelock) + {} + + function votingDelay() public pure virtual override returns (uint256) { + return 1 days; + } + + function votingPeriod() public pure virtual override returns (uint256) { + return 1 weeks; + } + + function proposalThreshold() public pure virtual override returns (uint256) { + return 0; + } + + // ... +} +``` + +### Disclaimer + +Timestamp based voting is a recent feature that was formalized in ERC-6372 and ERC-5805, and introduced in v4.9. At the time this feature is released, some governance tooling may not support it yet. Users can expect invalid reporting of deadlines & durations if the tool is not able to interpret the TRC6372 clock. This invalid reporting by offchain tools does not affect the onchain security and functionality of the governance contract. + +Governors with timestamp support (v4.9 and above) are compatible with old tokens (before v4.9) and will operate in "block number" mode (which is the mode all old tokens operate on). On the other hand, old Governor instances (before v4.9) are not compatible with new tokens operating using timestamps. If you update your token code to use timestamps, make sure to also update your Governor code. diff --git a/content/tron-contracts/index.mdx b/content/tron-contracts/index.mdx new file mode 100644 index 00000000..b5ca8ce5 --- /dev/null +++ b/content/tron-contracts/index.mdx @@ -0,0 +1,99 @@ +--- +title: "OpenZeppelin Contracts for Tron" +--- + +**A library for secure smart contract development.** Build on a solid foundation of community-vetted code. + +* Implementations of standards like [TRC20](/tron-contracts/trc20) and [TRC721](/tron-contracts/trc721). +* Flexible [role-based permissioning](/tron-contracts/access-control) scheme. +* Reusable [Solidity components](/tron-contracts/utilities) to build custom contracts and complex decentralized systems. + + +OpenZeppelin Contracts uses semantic versioning to communicate backwards compatibility of its API and storage layout. For upgradeable contracts, the storage layout of different major versions should be assumed incompatible, for example, it is unsafe to upgrade from 4.9.3 to 5.0.0. Learn more at [Backwards Compatibility](/tron-contracts/backwards-compatibility). + + +## Overview + +### Release Tags + +We use NPM tags to clearly distinguish between audited and non-audited versions of our package: + +| **Tag** | **Purpose** | **Description** | +| --- | --- | --- | +| **latest** | ✅ Audited releases | Stable, audited versions of the package. This is the **default** version installed when users run `npm install @openzeppelin/tron-contracts`. | +| **dev** | 🧪 Final but not audited | Versions that are finalized and feature-complete but have **not yet been audited**. This version is fully tested, can be used in production and is covered by the bug bounty. | +| **next** | 🚧 Release candidates | Pre-release versions that are **not final**. Used for testing and validation before the version becomes a final `dev` or `latest` release. | + +### Installation + +#### Hardhat (npm) + +```console +$ npm install @openzeppelin/tron-contracts +``` +→ Installs the latest audited release (`latest`). + +``` +$ npm install @openzeppelin/tron-contracts@dev +``` +→ Installs the latest unaudited release (`dev`). + +#### Foundry (git) + + +When installing via git, it is a common error to use the `master` branch. This is a development branch that should be avoided in favor of tagged releases. The release process involves security measures that the `master` branch does not guarantee. + + + +Foundry installs the latest version initially, but subsequent `forge update` commands will use the `master` branch. + + +```console +$ forge install OpenZeppelin/tron-contracts +``` + +Add `@openzeppelin/tron-contracts/=lib/tron-contracts/contracts/` in `remappings.txt.` + +### Usage + +Once installed, you can use the contracts in the library by importing them: + +```solidity +// contracts/MyNFT.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {TRC721} from "@openzeppelin/tron-contracts/token/TRC721/TRC721.sol"; + +contract MyNFT is TRC721 { + constructor() TRC721("MyNFT", "MNFT") {} +} +``` + + +If you're new to smart contract development, head to Developing Smart Contracts to learn about creating a new project and compiling your contracts. + + +To keep your system secure, you should **always** use the installed code as-is, and neither copy-paste it from online sources, nor modify it yourself. The library is designed so that only the contracts and functions you use are deployed, so you don't need to worry about it needlessly increasing gas costs. + +## Security + +Please report any security issues you find via our [bug bounty program on Immunefi](https://www.immunefi.com/bounty/openzeppelin) or directly to security@openzeppelin.org. + +The [Security Center](https://contracts.openzeppelin.com/security) contains more details about the secure development process. + +## Learn More + +The guides in the sidebar will teach about different concepts, and how to use the related contracts that OpenZeppelin Contracts provides: + +* [Access Control](/tron-contracts/access-control): decide who can perform each of the actions on your system. +* [Tokens](/tron-contracts/tokens): create tradable assets or collectibles, like the well known [TRC20](/tron-contracts/trc20) and [TRC721](/tron-contracts/trc721) standards. +* [Utilities](/tron-contracts/utilities): generic useful tools, including non-overflowing math, signature verification, and trustless paying systems. + +The full API is also thoroughly documented, and serves as a great reference when developing your smart contract application. You can also ask for help or follow Contracts' development in the [community forum](https://forum.openzeppelin.com). + +The following articles provide great background reading, though please note, some of the referenced tools have changed as the tooling in the ecosystem continues to rapidly evolve. + +* [The Hitchhiker’s Guide to Smart Contracts in Ethereum](https://blog.openzeppelin.com/the-hitchhikers-guide-to-smart-contracts-in-ethereum-848f08001f05) will help you get an overview of the various tools available for smart contract development, and help you set up your environment. +* [A Gentle Introduction to Ethereum Programming, Part 1](https://blog.openzeppelin.com/a-gentle-introduction-to-ethereum-programming-part-1-783cc7796094) provides very useful information on an introductory level, including many basic concepts from the Ethereum platform. +* For a more in-depth dive, you may read the guide [Designing the architecture for your Ethereum application](https://blog.openzeppelin.com/designing-the-architecture-for-your-ethereum-application-9cec086f8317), which discusses how to better structure your application and its relationship to the real world. diff --git a/content/tron-contracts/tokens.mdx b/content/tron-contracts/tokens.mdx new file mode 100644 index 00000000..3f03cd19 --- /dev/null +++ b/content/tron-contracts/tokens.mdx @@ -0,0 +1,31 @@ +--- +title: "Tokens" +--- + +Ah, the "token": blockchain's most powerful and most misunderstood tool. + +A token is a _representation of something in the blockchain_. This something can be money, time, services, shares in a company, a virtual pet, anything. By representing things as tokens, we can allow smart contracts to interact with them, exchange them, create or destroy them. + +## But First, ~~Coffee~~ a Primer on Token Contracts + +Much of the confusion surrounding tokens comes from two concepts getting mixed up: _token contracts_ and the actual _tokens_. + +A _token contract_ is simply an Ethereum smart contract. "Sending tokens" actually means "calling a method on a smart contract that someone wrote and deployed". At the end of the day, a token contract is not much more than a mapping of addresses to balances, plus some methods to add and subtract from those balances. + +It is these balances that represent the _tokens_ themselves. Someone "has tokens" when their balance in the token contract is non-zero. That's it! These balances could be considered money, experience points in a game, deeds of ownership, or voting rights, and each of these tokens would be stored in different token contracts. + +## Different Kinds of Tokens + +Note that there's a big difference between having two voting rights and two deeds of ownership: each vote is equal to all others, but houses usually are not! This is called [fungibility](https://en.wikipedia.org/wiki/Fungibility). _Fungible goods_ are equivalent and interchangeable, like Ether, fiat currencies, and voting rights. _Non-fungible_ goods are unique and distinct, like deeds of ownership, or collectibles. + +In a nutshell, when dealing with non-fungibles (like your house) you care about _which ones_ you have, while in fungible assets (like your bank account statement) what matters is _how much_ you have. + +## Standards + +Even though the concept of a token is simple, they have a variety of complexities in the implementation. Because everything in Ethereum is just a smart contract, and there are no rules about what smart contracts have to do, the community has developed a variety of **standards** (called EIPs or ERCs) for documenting how a contract can interoperate with other contracts. + +You've probably heard of the TRC-20 or TRC-721 token standards, and that's why you're here. Head to our specialized guides to learn more about these: + +* [TRC-20](/tron-contracts/trc20): the most widespread token standard for fungible assets, albeit somewhat limited by its simplicity. +* [TRC-721](/tron-contracts/trc721): the de-facto solution for non-fungible tokens, often used for collectibles and games. +* [TRC-1155](/tron-contracts/trc1155): a novel standard for multi-tokens, allowing for a single contract to represent multiple fungible and non-fungible tokens, along with batched operations for increased gas efficiency. diff --git a/content/tron-contracts/trc1155.mdx b/content/tron-contracts/trc1155.mdx new file mode 100644 index 00000000..0c306616 --- /dev/null +++ b/content/tron-contracts/trc1155.mdx @@ -0,0 +1,146 @@ +--- +title: "TRC-1155" +--- + +TRC-1155 is a novel token standard that aims to take the best from previous standards to create a [**fungibility-agnostic**](/tron-contracts/tokens#different-kinds-of-tokens) and **gas-efficient** [token contract](/tron-contracts/tokens#but_first_coffee_a_primer_on_token_contracts). + + +TRC-1155 draws ideas from all of [TRC-20](/tron-contracts/trc20), [TRC-721](/tron-contracts/trc721), and [ERC-777](https://eips.ethereum.org/EIPS/eip-777). If you're unfamiliar with those standards, head to their guides before moving on. + + +## Multi Token Standard + +The distinctive feature of TRC-1155 is that it uses a single smart contract to represent multiple tokens at once. This is why its `balanceOf` function differs from TRC-20's and ERC-777's: it has an additional `id` argument for the identifier of the token that you want to query the balance of. + +This is similar to how TRC-721 does things, but in that standard a token `id` has no concept of balance: each token is non-fungible and exists or doesn't. The TRC-721 `balanceOf` function refers to _how many different tokens_ an account has, not how many of each. On the other hand, in TRC-1155 accounts have a distinct balance for each token `id`, and non-fungible tokens are implemented by simply minting a single one of them. + +This approach leads to massive gas savings for projects that require multiple tokens. Instead of deploying a new contract for each token type, a single TRC-1155 token contract can hold the entire system state, reducing deployment costs and complexity. + +## Batch Operations + +Because all state is held in a single contract, it is possible to operate over multiple tokens in a single transaction very efficiently. The standard provides two functions, `balanceOfBatch` and `safeBatchTransferFrom`, that make querying multiple balances and transferring multiple tokens simpler and less gas-intensive. + +In the spirit of the standard, we've also included batch operations in the non-standard functions, such as `_mintBatch`. + +## Constructing a TRC-1155 Token Contract + +We'll use TRC-1155 to track multiple items in our game, which will each have their own unique attributes. We mint all items to the deployer of the contract, which we can later transfer to players. Players are free to keep their tokens or trade them with other people as they see fit, as they would any other asset on the blockchain! + +For simplicity, we will mint all items in the constructor, but you could add minting functionality to the contract to mint on demand to players. + + +For an overview of minting mechanisms, check out [Creating TRC-20 Supply](/tron-contracts/trc20-supply). + + +Here's what a contract for tokenized items might look like: + +```solidity +// contracts/GameItems.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {TRC1155} from "@openzeppelin/tron-contracts/token/TRC1155/TRC1155.sol"; + +contract GameItems is TRC1155 { + uint256 public constant GOLD = 0; + uint256 public constant SILVER = 1; + uint256 public constant THORS_HAMMER = 2; + uint256 public constant SWORD = 3; + uint256 public constant SHIELD = 4; + + constructor() TRC1155("https://game.example/api/item/{id}.json") { + _mint(msg.sender, GOLD, 10 ** 18, ""); + _mint(msg.sender, SILVER, 10 ** 27, ""); + _mint(msg.sender, THORS_HAMMER, 1, ""); + _mint(msg.sender, SWORD, 10 ** 9, ""); + _mint(msg.sender, SHIELD, 10 ** 9, ""); + } +} +``` + +Note that for our Game Items, Gold is a fungible token whilst Thor's Hammer is a non-fungible token as we minted only one. + +The `TRC1155` contract includes the optional extension `ITRC1155MetadataURI`. That's where the `uri` function comes from: we use it to retrieve the metadata uri. + +Also note that, unlike TRC-20, TRC-1155 lacks a `decimals` field, since each token is distinct and cannot be partitioned. + +Once deployed, we will be able to query the deployer's balance: + +```javascript +> gameItems.balanceOf(deployerAddress,3) +1000000000 +``` + +We can transfer items to player accounts: + +```javascript +> gameItems.safeTransferFrom(deployerAddress, playerAddress, 2, 1, "0x0") +> gameItems.balanceOf(playerAddress, 2) +1 +> gameItems.balanceOf(deployerAddress, 2) +0 +``` + +We can also batch transfer items to player accounts and get the balance of batches: + +```javascript +> gameItems.safeBatchTransferFrom(deployerAddress, playerAddress, [0,1,3,4], [50,100,1,1], "0x0") +> gameItems.balanceOfBatch([playerAddress,playerAddress,playerAddress,playerAddress,playerAddress], [0,1,2,3,4]) +[50,100,1,1,1] +``` + +The metadata uri can be obtained: + +```javascript +> gameItems.uri(2) +"https://game.example/api/item/{id}.json" +``` + +The `uri` can include the string `{id}` which clients must replace with the actual token ID, in lowercase hexadecimal (with no 0x prefix) and leading zero padded to 64 hex characters. + +For token ID `2` and uri `https://game.example/api/item/{id}.json` clients would replace `{id}` with `0000000000000000000000000000000000000000000000000000000000000002` to retrieve JSON at `https://game.example/api/item/0000000000000000000000000000000000000000000000000000000000000002.json`. + +The JSON document for token ID 2 might look something like: + +```json +{ + "name": "Thor's hammer", + "description": "Mjölnir, the legendary hammer of the Norse god of thunder.", + "image": "https://game.example/item-id-8u5h2m.png", + "strength": 20 +} +``` + +For more information about the metadata JSON Schema, check out the [TRC-1155 Metadata URI JSON Schema](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1155.md#erc-1155-metadata-uri-json-schema). + + +You'll notice that the item's information is included in the metadata, but that information isn't on-chain! So a game developer could change the underlying metadata, changing the rules of the game! + + + +If you'd like to put all item information on-chain, you can override the `uri()` function in your TRC-1155 contract (or use `TRC1155URIStorage`) to return a [`Base64`](/tron-contracts/utilities#base64) Data URI with the JSON schema encoded, though it will be rather costly. You could also leverage IPFS to store the URI information, but these techniques are out of the scope of this overview guide + + +## Sending Tokens to Contracts + +A key difference when using `safeTransferFrom` is that token transfers to other contracts may revert with the following custom error: + +```text +TRC1155InvalidReceiver("
") +``` + +This is a good thing! It means that the recipient contract has not registered itself as aware of the TRC-1155 protocol, so transfers to it are disabled to **prevent tokens from being locked forever**. As an example, [the Golem contract currently holds over 350k `GNT` tokens](https://etherscan.io/token/0xa74476443119A942dE498590Fe1f2454d7D4aC0d?a=0xa74476443119A942dE498590Fe1f2454d7D4aC0d), and lacks methods to get them out of there. This has happened to virtually every TRC20-backed project, usually due to user error. + +In order for our contract to receive TRC-1155 tokens we can inherit from the convenience contract `TRC1155Holder` which handles the registering for us. However, we need to remember to implement functionality to allow tokens to be transferred out of our contract: + +```solidity +// contracts/MyTRC1155HolderContract.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {TRC1155Holder} from "@openzeppelin/tron-contracts/token/TRC1155/utils/TRC1155Holder.sol"; + +contract MyTRC1155HolderContract is TRC1155Holder {} +``` + +We can also implement more complex scenarios using the `onTRC1155Received` and `onTRC1155BatchReceived` functions. diff --git a/content/tron-contracts/trc20-supply.mdx b/content/tron-contracts/trc20-supply.mdx new file mode 100644 index 00000000..5f359d77 --- /dev/null +++ b/content/tron-contracts/trc20-supply.mdx @@ -0,0 +1,87 @@ +--- +title: "Creating TRC-20 Supply" +--- + +In this guide, you will learn how to create a TRC-20 token with a custom supply mechanism. We will showcase two idiomatic ways to use OpenZeppelin Contracts for this purpose that you will be able to apply to your smart contract development practice. + +The standard interface implemented by tokens built on Ethereum is called TRC-20, and Contracts includes a widely used implementation of it: the aptly named `TRC20` contract. This contract, like the standard itself, is quite simple and bare-bones. In fact, if you try to deploy an instance of `TRC20` as-is it will be quite literally useless... it will have no supply! What use is a token with no supply? + +The way that supply is created is not defined in the TRC-20 document. Every token is free to experiment with its own mechanisms, ranging from the most decentralized to the most centralized, from the most naive to the most researched, and more. + +## Fixed Supply + +Let's say we want a token with a fixed supply of 1000, initially allocated to the account that deploys the contract. If you've used Contracts v1, you may have written code like the following: + +```solidity +contract TRC20FixedSupply is TRC20 { + constructor() { + totalSupply += 1000; + balances[msg.sender] += 1000; + } +} +``` + +Starting with Contracts v2, this pattern is not only discouraged, but disallowed. The variables `totalSupply` and `balances` are now private implementation details of `TRC20`, and you can't directly write to them. Instead, there is an internal `_mint` function that will do exactly this: + +```solidity +contract TRC20FixedSupply is TRC20 { + constructor() TRC20("Fixed", "FIX") { + _mint(msg.sender, 1000); + } +} +``` + +Encapsulating state like this makes it safer to extend contracts. For instance, in the first example we had to manually keep the `totalSupply` in sync with the modified balances, which is easy to forget. In fact, we omitted something else that is also easily forgotten: the `Transfer` event that is required by the standard, and which is relied on by some clients. The second example does not have this bug, because the internal `_mint` function takes care of it. + +## Rewarding Block Proposers + +The internal `_mint` function is the key building block that allows us to write TRC-20 extensions that implement a supply mechanism. + +The mechanism we will implement is a token reward for the block proposer (the address that receives transaction fees). In Solidity, we can access this address using the global variable `block.coinbase`. Note that in Proof of Stake networks, `block.coinbase` refers to the fee recipient address, not a miner. We will mint a token reward to this address whenever someone calls the function `mintMinerReward()` on our token. The mechanism may sound silly, but you never know what kind of dynamic this might result in, and it's worth analyzing and experimenting with! + +```solidity +contract TRC20WithMinerReward is TRC20 { + constructor() TRC20("Reward", "RWD") {} + + function mintMinerReward() public { + _mint(block.coinbase, 1000); + } +} +``` + +As we can see, `_mint` makes it super easy to do this correctly. + +## Automating the Reward + +So far our supply mechanism was triggered manually, but `TRC20` also allows us to extend the core functionality of the token through the `_update` function. + +Adding to the supply mechanism from the previous section, we can use this function to mint a block proposer reward for every token transfer that is included in the blockchain. + +```solidity +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {TRC20} from "@openzeppelin/tron-contracts/token/TRC20/TRC20.sol"; + +contract TRC20WithAutoMinerReward is TRC20 { + constructor() TRC20("Reward", "RWD") { + _mintMinerReward(); + } + + function _mintMinerReward() internal { + _mint(block.coinbase, 1000); + } + + function _update(address from, address to, uint256 value) internal virtual override { + if (!(from == address(0) && to == block.coinbase)) { + _mintMinerReward(); + } + super._update(from, to, value); + } +} +``` + +## Wrapping Up + +We've seen how to implement a TRC-20 supply mechanism: internally through `_mint`. Hopefully this has helped you understand how to use OpenZeppelin Contracts and some of the design principles behind it, and you can apply them to your own smart contracts. diff --git a/content/tron-contracts/trc20.mdx b/content/tron-contracts/trc20.mdx new file mode 100644 index 00000000..c4c479f7 --- /dev/null +++ b/content/tron-contracts/trc20.mdx @@ -0,0 +1,78 @@ +--- +title: "TRC-20" +--- + +A TRC-20 token contract keeps track of [_fungible_ tokens](/tron-contracts/tokens#different-kinds-of-tokens): any one token is exactly equal to any other token; no tokens have special rights or behavior associated with them. This makes TRC-20 tokens useful for things like a **medium of exchange currency**, **voting rights**, **staking**, and more. + +OpenZeppelin Contracts provides many TRC20-related contracts. On the `API reference` you'll find detailed information on their properties and usage. + +## Constructing a TRC-20 Token Contract + +Using Contracts, we can easily create our own TRC-20 token contract, which will be used to track _Gold_ (GLD), an internal currency in a hypothetical game. + +Here's what our GLD token might look like. + +```solidity +// contracts/GLDToken.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {TRC20} from "@openzeppelin/tron-contracts/token/TRC20/TRC20.sol"; + +contract GLDToken is TRC20 { + constructor(uint256 initialSupply) TRC20("Gold", "GLD") { + _mint(msg.sender, initialSupply); + } +} +``` + +Our contracts are often used via [inheritance](https://solidity.readthedocs.io/en/latest/contracts.html#inheritance), and here we're reusing `TRC20` for both the basic standard implementation and the `name`, `symbol`, and `decimals` optional extensions. Additionally, we're creating an `initialSupply` of tokens, which will be assigned to the address that deploys the contract. + + +For a more complete discussion of TRC-20 supply mechanisms, see [Creating TRC-20 Supply](/tron-contracts/trc20-supply). + + +That's it! Once deployed, we will be able to query the deployer's balance: + +```javascript +> GLDToken.balanceOf(deployerAddress) +1000000000000000000000 +``` + +We can also transfer these tokens to other accounts: + +```javascript +> GLDToken.transfer(otherAddress, 300000000000000000000) +> GLDToken.balanceOf(otherAddress) +300000000000000000000 +> GLDToken.balanceOf(deployerAddress) +700000000000000000000 +``` + +## A Note on `decimals` + +Often, you'll want to be able to divide your tokens into arbitrary amounts: say, if you own `5 GLD`, you may want to send `1.5 GLD` to a friend, and keep `3.5 GLD` to yourself. Unfortunately, Solidity and the EVM do not support this behavior: only integer (whole) numbers can be used, which poses an issue. You may send `1` or `2` tokens, but not `1.5`. + +To work around this, `TRC20` provides a `decimals` field, which is used to specify how many decimal places a token has. To be able to transfer `1.5 GLD`, `decimals` must be at least `1`, since that number has a single decimal place. + +How can this be achieved? It's actually very simple: a token contract can use larger integer values, so that a balance of `50` will represent `5 GLD`, a transfer of `15` will correspond to `1.5 GLD` being sent, and so on. + +It is important to understand that `decimals` is _only used for display purposes_. All arithmetic inside the contract is still performed on integers, and it is the different user interfaces (wallets, exchanges, etc.) that must adjust the displayed values according to `decimals`. The total token supply and balance of each account are not specified in `GLD`: you need to divide by `10 ** decimals` to get the actual `GLD` amount. + +You'll probably want to use a `decimals` value of `18`, just like Ether and most TRC-20 token contracts in use, unless you have a very special reason not to. When minting tokens or transferring them around, you will be actually sending the number `num GLD * (10 ** decimals)`. + + +By default, `TRC20` uses a value of `18` for `decimals`. To use a different value, you will need to override the `decimals()` function in your contract. + + +```solidity +function decimals() public view virtual override returns (uint8) { + return 16; +} +``` + +So if you want to send `5` tokens using a token contract with 18 decimals, the method to call will actually be: + +```solidity +transfer(recipient, 5 * (10 ** 18)); +``` diff --git a/content/tron-contracts/trc4626.mdx b/content/tron-contracts/trc4626.mdx new file mode 100644 index 00000000..2d003b88 --- /dev/null +++ b/content/tron-contracts/trc4626.mdx @@ -0,0 +1,272 @@ +--- +title: "TRC-4626" +--- + +[TRC-4626](https://github.com/tronprotocol/tips/blob/master/tip-4626.md) (TIP-4626, the TRON-side analogue of [EIP-4626](https://eips.ethereum.org/EIPS/eip-4626)) is an extension of [TRC-20](/tron-contracts/trc20) that proposes a standard interface for token vaults. This standard interface can be used by widely different contracts (including lending markets, aggregators, and intrinsically interest bearing tokens), which brings a number of subtleties. Navigating these potential issues is essential to implementing a compliant and composable token vault. + +We provide a base implementation of TRC-4626 that includes a simple vault. This contract is designed in a way that allows developers to easily re-configure the vault's behavior, with minimal overrides, while staying compliant. In this guide, we will discuss some security considerations that affect TRC-4626. We will also discuss common customizations of the vault. + +## Security concern: Inflation attack + +### Visualizing the vault + +In exchange for the assets deposited into a TRC-4626 vault, a user receives shares. These shares can later be burned to redeem the corresponding underlying assets. The number of shares a user gets depends on the amount of assets they put in and on the exchange rate of the vault. This exchange rate is defined by the current liquidity held by the vault. + +- If a vault has 100 tokens to back 200 shares, then each share is worth 0.5 assets. +- If a vault has 200 tokens to back 100 shares, then each share is worth 2.0 assets. + +In other words, the exchange rate can be defined as the slope of the line that passes through the origin and the current number of assets and shares in the vault. Deposits and withdrawals move the vault in this line. + +![Exchange rates in linear scale](/tron/erc4626-rate-linear.png) + +When plotted in log-log scale, the rate is defined similarly, but appears differently (because the point (0,0) is infinitely far away). Rates are represented by "diagonal" lines with different offsets. + +![Exchange rates in logarithmic scale](/tron/erc4626-rate-loglog.png) + +In such a representation, widely different rates can be clearly visible in the same graph. This wouldn't be the case in linear scale. + +![More exchange rates in logarithmic scale](/tron/erc4626-rate-loglogext.png) + +### The attack + +When depositing tokens, the number of shares a user gets is rounded towards zero. This rounding takes away value from the user in favor of the vault (i.e. in favor of all the current shareholders). This rounding is often negligible because of the amount at stake. If you deposit 1e9 shares worth of tokens, the rounding will have you lose at most 0.0000001% of your deposit. However if you deposit 10 shares worth of tokens, you could lose 10% of your deposit. Even worse, if you deposit `<1` share worth of tokens, then you get 0 shares, and you basically made a donation. + +For a given amount of assets, the more shares you receive the safer you are. If you want to limit your losses to at most 1%, you need to receive at least 100 shares. + +![Depositing assets](/tron/erc4626-deposit.png) + +In the figure we can see that for a given deposit of 500 assets, the number of shares we get and the corresponding rounding losses depend on the exchange rate. If the exchange rate is that of the orange curve, we are getting less than a share, so we lose 100% of our deposit. However, if the exchange rate is that of the green curve, we get 5000 shares, which limits our rounding losses to at most 0.02%. + +![Minting shares](/tron/erc4626-mint.png) + +Symmetrically, if we focus on limiting our losses to a maximum of 0.5%, we need to get at least 200 shares. With the green exchange rate that requires just 20 tokens, but with the orange rate that requires 200000 tokens. + +We can clearly see that the blue and green curves correspond to vaults that are safer than the yellow and orange curves. + +The idea of an inflation attack is that an attacker can donate assets to the vault to move the rate curve to the right, and make the vault unsafe. + +![Inflation attack without protection](/tron/erc4626-attack.png) + +Figure 6 shows how an attacker can manipulate the rate of an empty vault. First the attacker must deposit a small amount of tokens (1 token) and follow up with a donation of 1e5 tokens directly to the vault to move the exchange rate "right". This puts the vault in a state where any deposit smaller than 1e5 would be completely lost to the vault. Given that the attacker is the only shareholder (from their donation), the attacker would steal all the tokens deposited. + +An attacker would typically wait for a user to do the first deposit into the vault, and would frontrun that operation with the attack described above. The risk is low, and the size of the "donation" required to manipulate the vault is equivalent to the size of the deposit that is being attacked. + +In math that gives: + +- `a_0` the attacker deposit +- `a_1` the attacker donation +- `u` the user deposit + +| | Assets | Shares | Rate | +| --- | --- | --- | --- | +| initial | `0` | `0` | - | +| after attacker's deposit | `a_0` | `a_0` | `1` | +| after attacker's donation | `a_0 + a_1` | `a_0` | `a_0 / (a_0 + a_1)` | + +This means a deposit of `u` will give `(u * a_0) / (a_0 + a_1)` shares. + +For the attacker to dilute that deposit to 0 shares, causing the user to lose all its deposit, it must ensure that + +``` +(u * a_0) / (a_0 + a_1) < 1 <=> u < 1 + a_1 / a_0 +``` + +Using `a_0 = 1` and `a_1 = u` is enough. So the attacker only needs `u + 1` assets to perform a successful attack. + +It is easy to generalize the above results to scenarios where the attacker is going after a smaller fraction of the user's deposit. In order to target `u / n`, the user needs to suffer rounding of a similar fraction, which means the user must receive at most `n` shares. This results in: + +``` +(u * a_0) / (a_0 + a_1) < n <=> u / n < 1 + a_1 / a_0 +``` + +In this scenario, the attack is `n` times less powerful (in how much it is stealing) and costs `n` times less to execute. In both cases, the amount of funds the attacker needs to commit is equivalent to its potential earnings. + +### Defending with a virtual offset + +The defense we propose is based on the approach used in [YieldBox](https://github.com/boringcrypto/YieldBox). It consists of two parts: + +- Use an offset between the "precision" of the representation of shares and assets. Said otherwise, we use more decimal places to represent the shares than the underlying token does to represent the assets. +- Include virtual shares and virtual assets in the exchange rate computation. These virtual assets enforce the conversion rate when the vault is empty. + +These two parts work together in enforcing the security of the vault. First, the increased precision corresponds to a high rate, which we saw is safer as it reduces the rounding error when computing the amount of shares. Second, the virtual assets and shares (in addition to simplifying a lot of the computations) capture part of the donation, making it unprofitable for a developer to perform an attack. + +Following the previous math definitions, we have: + +- `δ` the vault offset +- `a_0` the attacker deposit +- `a_1` the attacker donation +- `u` the user deposit + +| | Assets | Shares | Rate | +| --- | --- | --- | --- | +| initial | `1` | `10^δ` | `10^δ` | +| after attacker's deposit | `1 + a_0` | `10^δ * (1 + a_0)` | `10^δ` | +| after attacker's donation | `1 + a_0 + a_1` | `10^δ * (1 + a_0)` | `10^δ * (1 + a_0) / (1 + a_0 + a_1)` | + +One important thing to note is that the attacker only owns a fraction `a_0 / (1 + a_0)` of the shares, so when doing the donation, he will only be able to recover that fraction `(a_1 * a_0) / (1 + a_0)` of the donation. The remaining `a_1 / (1 + a_0)` are captured by the vault. + +``` +loss = a_1 / (1 + a_0) +``` + +When the user deposits `u`, he receives + +``` +10^δ * u * (1 + a_0) / (1 + a_0 + a_1) +``` + +For the attacker to dilute that deposit to 0 shares, causing the user to lose all its deposit, it must ensure that + +``` +10^δ * u * (1 + a_0) / (1 + a_0 + a_1) < 1 +<=> 10^δ * u < (1 + a_0 + a_1) / (1 + a_0) +<=> 10^δ * u < 1 + a_1 / (1 + a_0) +<=> 10^δ * u <= loss +``` + +- If the offset is 0, the attacker's loss is at least equal to the user's deposit. +- If the offset is greater than 0, the attacker will have to suffer losses that are orders of magnitude bigger than the amount of value that can hypothetically be stolen from the user. + +This shows that even with an offset of 0, the virtual shares and assets make this attack non profitable for the attacker. Bigger offsets increase the security even further by making any attack on the user extremely wasteful. + +The following figure shows how the offset impacts the initial rate and limits the ability of an attacker with limited funds to inflate it effectively. + +![Inflation attack without offset=3](/tron/erc4626-attack-3a.png) + +`δ = 3`, `a_0 = 1`, `a_1 = 10^5` + +![Inflation attack without offset=3 and an attacker deposit that limits its losses](/tron/erc4626-attack-3b.png) + +`δ = 3`, `a_0 = 100`, `a_1 = 10^5` + +![Inflation attack without offset=6](/tron/erc4626-attack-6.png) + +`δ = 6`, `a_0 = 1`, `a_1 = 10^5` + +## Custom behavior: Adding fees to the vault + +In a TRC-4626 vaults, fees can be captured during the deposit/mint and/or during the withdraw/redeem steps. In both cases it is essential to remain compliant with the TRC-4626 requirements with regard to the preview functions. + +For example, if calling `deposit(100, receiver)`, the caller should deposit exactly 100 underlying tokens, including fees, and the receiver should receive a number of shares that matches the value returned by `previewDeposit(100)`. Similarly, `previewMint` should account for the fees that the user will have to pay on top of share's cost. + +As for the `Deposit` event, while this is less clear in the EIP spec itself, there seems to be consensus that it should include the number of assets paid for by the user, including the fees. + +On the other hand, when withdrawing assets, the number given by the user should correspond to what he receives. Any fees should be added to the quote (in shares) performed by `previewWithdraw`. + +The `Withdraw` event should include the number of shares the user burns (including fees) and the number of assets the user actually receives (after fees are deducted). + +The consequence of this design is that both the `Deposit` and `Withdraw` events will describe two exchange rates. The spread between the "Buy-in" and the "Exit" prices correspond to the fees taken by the vault. + +The following example describes how fees proportional to the deposited/withdrawn amount can be implemented: + +```solidity +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {ITRC20} from "@openzeppelin/tron-contracts/token/TRC20/ITRC20.sol"; +import {TRC4626} from "@openzeppelin/tron-contracts/token/TRC20/extensions/TRC4626.sol"; +import {SafeTRC20} from "@openzeppelin/tron-contracts/token/TRC20/utils/SafeTRC20.sol"; +import {Math} from "@openzeppelin/tron-contracts/utils/math/Math.sol"; + +/// @dev TRC-4626 vault with entry/exit fees expressed in https://en.wikipedia.org/wiki/Basis_point[basis point (bp)]. +/// +/// NOTE: The contract charges fees in terms of assets, not shares. This means that the fees are calculated based on the +/// amount of assets that are being deposited or withdrawn, and not based on the amount of shares that are being minted or +/// redeemed. This is an opinionated design decision that should be taken into account when integrating this contract. +/// +/// WARNING: This contract has not been audited and shouldn't be considered production ready. Consider using it with caution. +abstract contract TRC4626Fees is TRC4626 { + using Math for uint256; + + uint256 private constant _BASIS_POINT_SCALE = 1e4; + + // === Overrides === + + /// @dev Preview taking an entry fee on deposit. See {ITRC4626-previewDeposit}. + function previewDeposit(uint256 assets) public view virtual override returns (uint256) { + uint256 fee = _feeOnTotal(assets, _entryFeeBasisPoints()); + return super.previewDeposit(assets - fee); + } + + /// @dev Preview adding an entry fee on mint. See {ITRC4626-previewMint}. + function previewMint(uint256 shares) public view virtual override returns (uint256) { + uint256 assets = super.previewMint(shares); + return assets + _feeOnRaw(assets, _entryFeeBasisPoints()); + } + + /// @dev Preview adding an exit fee on withdrawal. See {ITRC4626-previewWithdraw}. + function previewWithdraw(uint256 assets) public view virtual override returns (uint256) { + uint256 fee = _feeOnRaw(assets, _exitFeeBasisPoints()); + return super.previewWithdraw(assets + fee); + } + + /// @dev Preview taking an exit fee on redeem. See {ITRC4626-previewRedeem}. + function previewRedeem(uint256 shares) public view virtual override returns (uint256) { + uint256 assets = super.previewRedeem(shares); + return assets - _feeOnTotal(assets, _exitFeeBasisPoints()); + } + + /// @dev Send entry fee to {_entryFeeRecipient}. See {TRC4626-_deposit}. + function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual override { + uint256 fee = _feeOnTotal(assets, _entryFeeBasisPoints()); + address recipient = _entryFeeRecipient(); + + super._deposit(caller, receiver, assets, shares); + + if (fee > 0 && recipient != address(this)) { + SafeTRC20.safeTransfer(ITRC20(asset()), recipient, fee); + } + } + + /// @dev Send exit fee to {_exitFeeRecipient}. See {TRC4626-_withdraw}. + function _withdraw( + address caller, + address receiver, + address owner, + uint256 assets, + uint256 shares + ) internal virtual override { + uint256 fee = _feeOnRaw(assets, _exitFeeBasisPoints()); + address recipient = _exitFeeRecipient(); + + super._withdraw(caller, receiver, owner, assets, shares); + + if (fee > 0 && recipient != address(this)) { + SafeTRC20.safeTransfer(ITRC20(asset()), recipient, fee); + } + } + + // === Fee configuration === + + function _entryFeeBasisPoints() internal view virtual returns (uint256) { + return 0; // replace with e.g. 100 for 1% + } + + function _exitFeeBasisPoints() internal view virtual returns (uint256) { + return 0; // replace with e.g. 100 for 1% + } + + function _entryFeeRecipient() internal view virtual returns (address) { + return address(0); // replace with e.g. a treasury address + } + + function _exitFeeRecipient() internal view virtual returns (address) { + return address(0); // replace with e.g. a treasury address + } + + // === Fee operations === + + /// @dev Calculates the fees that should be added to an amount `assets` that does not already include fees. + /// Used in {ITRC4626-mint} and {ITRC4626-withdraw} operations. + function _feeOnRaw(uint256 assets, uint256 feeBasisPoints) private pure returns (uint256) { + return assets.mulDiv(feeBasisPoints, _BASIS_POINT_SCALE, Math.Rounding.Ceil); + } + + /// @dev Calculates the fee part of an amount `assets` that already includes fees. + /// Used in {ITRC4626-deposit} and {ITRC4626-redeem} operations. + function _feeOnTotal(uint256 assets, uint256 feeBasisPoints) private pure returns (uint256) { + return assets.mulDiv(feeBasisPoints, feeBasisPoints + _BASIS_POINT_SCALE, Math.Rounding.Ceil); + } +} +``` diff --git a/content/tron-contracts/trc6909.mdx b/content/tron-contracts/trc6909.mdx new file mode 100644 index 00000000..add5211f --- /dev/null +++ b/content/tron-contracts/trc6909.mdx @@ -0,0 +1,77 @@ +--- +title: "TRC-6909" +--- + +TRC-6909 is a draft EIP that draws on TRC-1155 learnings since it was published in 2018. The main goals of TRC-6909 are to decrease gas costs and complexity--this is mainly accomplished by removing batching and callbacks. + + +To understand the inspiration for a multi token standard, see the [multi token standard](/tron-contracts/trc1155#multi-token-standard) section within the TRC-1155 docs. + + +## Changes from TRC-1155 + +There are three main changes from TRC-1155 which are as follows: + +1. The removal of batch operations. +2. The removal of transfer callbacks. +3. Granularization in approvals--approvals can be set globally (as operators) or as amounts per token (inspired by TRC20). + +## Constructing a TRC-6909 Token Contract + +We'll use TRC-6909 to track multiple items in a game, each having their own unique attributes. All item types will be minted to the deployer of the contract, which we can later transfer to players. We'll also use the `TRC6909Metadata` extension to add decimals to our fungible items (the vanilla TRC-6909 implementation does not have decimals). + +For simplicity, we will mint all items in the constructor--however, minting functionality could be added to the contract to mint on demand to players. + + +For an overview of minting mechanisms, check out [Creating TRC-20 Supply](/tron-contracts/trc20-supply). + + +Here's what a contract for tokenized items might look like: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {TRC6909Metadata} from "@openzeppelin/tron-contracts/token/TRC6909/extensions/TRC6909Metadata.sol"; + +contract TRC6909GameItems is TRC6909Metadata { + uint256 public constant GOLD = 0; + uint256 public constant SILVER = 1; + uint256 public constant THORS_HAMMER = 2; + uint256 public constant SWORD = 3; + uint256 public constant SHIELD = 4; + + constructor() { + _setDecimals(GOLD, 18); + _setDecimals(SILVER, 18); + // Default decimals is 0 + _setDecimals(SWORD, 9); + _setDecimals(SHIELD, 9); + + _mint(msg.sender, GOLD, 10 ** 18); + _mint(msg.sender, SILVER, 10_000 ** 18); + _mint(msg.sender, THORS_HAMMER, 1); + _mint(msg.sender, SWORD, 10 ** 9); + _mint(msg.sender, SHIELD, 10 ** 9); + } +} +``` + +Note that there is no content URI functionality in the base implementation, but the `TRC6909ContentURI` extension adds it. Additionally, the base implementation does not track total supplies, but the `TRC6909TokenSupply` extension tracks the total supply of each token id. + +Once the contract is deployed, we will be able to query the deployer's balance: + +```javascript +> gameItems.balanceOf(deployerAddress, 3) +1000000000 +``` + +We can transfer items to player accounts: + +```javascript +> gameItems.transfer(playerAddress, 2, 1) +> gameItems.balanceOf(playerAddress, 2) +1 +> gameItems.balanceOf(deployerAddress, 2) +0 +``` diff --git a/content/tron-contracts/trc721.mdx b/content/tron-contracts/trc721.mdx new file mode 100644 index 00000000..bfee86b5 --- /dev/null +++ b/content/tron-contracts/trc721.mdx @@ -0,0 +1,78 @@ +--- +title: "TRC-721" +--- + +We've discussed how you can make a _fungible_ token using [TRC-20](/tron-contracts/trc20), but what if not all tokens are alike? This comes up in situations like **real estate**, **voting rights**, or **collectibles**, where some items are valued more than others, due to their usefulness, rarity, etc. TRC-721 is a standard for representing ownership of [_non-fungible_ tokens](/tron-contracts/tokens#different-kinds-of-tokens), that is, where each token is unique. + +TRC-721 is a more complex standard than TRC-20, with multiple optional extensions, and is split across a number of contracts. The OpenZeppelin Contracts provide flexibility regarding how these are combined, along with custom useful extensions. Check out the API Reference to learn more about these. + +## Constructing a TRC-721 Token Contract + +We'll use TRC-721 to track items in our game, which will each have their own unique attributes. Whenever one is to be awarded to a player, it will be minted and sent to them. Players are free to keep their token or trade it with other people as they see fit, as they would any other asset on the blockchain! Please note that any account can call `awardItem` to mint items. To restrict what accounts can mint items we can add [Access Control](/tron-contracts/access-control). + +Here's what a contract for tokenized items might look like: + +```solidity +// contracts/GameItem.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {TRC721URIStorage, TRC721} from "@openzeppelin/tron-contracts/token/TRC721/extensions/TRC721URIStorage.sol"; + +contract GameItem is TRC721URIStorage { + uint256 private _nextTokenId; + + constructor() TRC721("GameItem", "ITM") {} + + function awardItem(address player, string memory tokenURI) public returns (uint256) { + uint256 tokenId = _nextTokenId++; + _mint(player, tokenId); + _setTokenURI(tokenId, tokenURI); + + return tokenId; + } +} +``` + +The `TRC721URIStorage` contract is an implementation of TRC-721 that includes the metadata standard extensions (`ITRC721Metadata`) as well as a mechanism for per-token metadata. That's where the `_setTokenURI` method comes from: we use it to store an item's metadata. + +Also note that, unlike TRC-20, TRC-721 lacks a `decimals` field, since each token is distinct and cannot be partitioned. + +New items can be created: + +```javascript +> gameItem.awardItem(playerAddress, "https://game.example/item-id-8u5h2m.json") +Transaction successful. Transaction hash: 0x... +Events emitted: + - Transfer(0x0000000000000000000000000000000000000000, playerAddress, 7) +``` + +And the owner and metadata of each item queried: + +```javascript +> gameItem.ownerOf(7) +playerAddress +> gameItem.tokenURI(7) +"https://game.example/item-id-8u5h2m.json" +``` + +This `tokenURI` should resolve to a JSON document that might look something like: + +```json +{ + "name": "Thor's hammer", + "description": "Mjölnir, the legendary hammer of the Norse god of thunder.", + "image": "https://game.example/item-id-8u5h2m.png", + "strength": 20 +} +``` + +For more information about the `tokenURI` metadata JSON Schema, check out the [TRC-721 specification](https://eips.ethereum.org/EIPS/eip-721). + + +You'll notice that the item's information is included in the metadata, but that information isn't on-chain! So a game developer could change the underlying metadata, changing the rules of the game! + + + +If you'd like to put all item information on-chain, you can extend TRC-721 to do so (though it will be rather costly) by providing a [`Base64`](/tron-contracts/utilities#base64) Data URI with the JSON schema encoded. You could also leverage IPFS to store the tokenURI information, but these techniques are out of the scope of this overview guide. + diff --git a/content/tron-contracts/tvm-differences.mdx b/content/tron-contracts/tvm-differences.mdx new file mode 100644 index 00000000..2c6f8153 --- /dev/null +++ b/content/tron-contracts/tvm-differences.mdx @@ -0,0 +1,34 @@ +--- +title: "TVM Differences" +--- + +The TRON Virtual Machine (TVM) is EVM-compatible, but it is **not identical**. A handful of differences are silent footguns: code that compiles and looks correct on Ethereum can produce the wrong address, accept unintended assets, or misvalue amounts on TRON. This page highlights the divergences that matter when building with OpenZeppelin Contracts for TRON and how the library handles each one. It is a summary — each adaptation is documented in detail in the affected contract's NatSpec. + + +Most standards are also **renamed** to their TRON identifiers (`ERC20` → `TRC20`, `ERC165` → `TRC165`, `EIP712` → `TIP712`, and so on). See the [Overview](/tron-contracts) for the full TRON ↔ Ethereum mapping. + + +## Summary + +| Area | TVM behaviour | What this library does | +| --- | --- | --- | +| `CREATE2` address derivation | Preimage uses a `0x41` prefix (TIP-26), not `0xff` | `Create2` and `Clones` predict addresses with the TRON prefix so predict-then-deploy flows resolve to the real address. | +| P256 / RIP-7212 precompile | The secp256r1 precompile is inactive on mainnet (gated behind a future fork) | `P256.verify` routes to the Solidity verifier; `verifyNative` is kept as an explicit native-only entry point. | +| EIP-7702 | Not supported on the TVM | The EIP-7702 utilities are not ported. | +| ERC-4337 | Compiles, but TRON has no native bundler / EntryPoint | Retained but treated as compile-only; account-abstraction flows are not functional today. | +| TRX decimals | TRX has **6** decimals, not 18 — so a `1 ether` literal evaluates to 10¹² TRX | Guidance and examples use SUN-denominated literals; avoid `ether` / `gwei` literals. | +| USDT-TRON `transfer` | Returns `false` (or no value) even on a successful transfer | `SafeTRC20.safeTransferUSDT` verifies the recipient's balance delta instead of trusting the return value. | +| TIP-712 / address format | Addresses are 21-byte, `0x41`-prefixed; off-chain SDKs sign over the 21-byte form | NatSpec advisories on the contracts whose typed-data or address handling crosses the signing boundary. | +| Divergent precompiles | Addresses `0x03` / `0x09` / `0x0A` are shadowed or repurposed (TIP-43, TIP-60); real RIPEMD-160 lives at `0x20003` | The library never hard-codes calls to these addresses. | +| Resource model | Execution is metered in **energy + bandwidth**, not gas; `tx.gasprice` returns the committee-set energy price | No change required — the library does not depend on EVM gas-forwarding rules. | +| `SELFDESTRUCT` (TIP-6780) | Matches EIP-6780 semantics, which strengthens `_disableInitializers` on the TVM | Documented via NatSpec; no behaviour is weakened. | +| Block cadence | ~3 second blocks, versus Ethereum's ~12 seconds | Prefer timestamp-based (EIP-6372) mode for time-bounded `Governor` windows rather than block counts. | + + +OpenZeppelin Contracts reads `block.chainid` dynamically, so TIP-712 domain separators are automatically correct for whichever TRON network you deploy to. + + +## Learn more + +- [Backwards Compatibility](/tron-contracts/backwards-compatibility) — versioning and storage-layout guarantees. +- [Using with Upgrades](/tron-contracts/upgradeable) — proxy patterns and upgrade safety on the TVM. diff --git a/content/tron-contracts/upgradeable.mdx b/content/tron-contracts/upgradeable.mdx new file mode 100644 index 00000000..9d864627 --- /dev/null +++ b/content/tron-contracts/upgradeable.mdx @@ -0,0 +1,80 @@ +--- +title: "Using with Upgrades" +--- + +If your contract is going to be deployed with upgradeability — for example behind an ERC-1967, UUPS, or transparent proxy — you will need to use the Upgradeable variant of OpenZeppelin Contracts for Tron. + +This variant is available as a separate package called `@openzeppelin/tron-contracts-upgradeable`, hosted at [OpenZeppelin/tron-contracts-upgradeable](https://github.com/OpenZeppelin/tron-contracts-upgradeable). It uses `@openzeppelin/tron-contracts` as a peer dependency, and is generated automatically from it by OpenZeppelin's [Upgradeability Transpiler](https://github.com/OpenZeppelin/openzeppelin-transpiler). + +It follows all of the standard rules for writing upgradeable contracts: constructors are replaced by initializer functions, state variables are initialized inside those initializers rather than inline, and contract state is kept in ERC-7201 namespaced storage so layouts stay compatible across upgrades. + + +OpenZeppelin's Hardhat and Foundry Upgrades Plugins target EVM chains and do not deploy to the Tron network. On Tron you deploy the proxy yourself with your Tron toolchain (see [deployment](#deploying-behind-a-proxy) below); the contract-authoring rules on this page apply unchanged. + + +## Overview + +### Installation + +```console +$ npm install @openzeppelin/tron-contracts-upgradeable @openzeppelin/tron-contracts +``` + +### Usage + +The Upgradeable package replicates the structure of the main OpenZeppelin Contracts package, but every file and contract has the suffix `Upgradeable`. + +```diff +-import {TRC721} from "@openzeppelin/tron-contracts/token/TRC721/TRC721.sol"; ++import {TRC721Upgradeable} from "@openzeppelin/tron-contracts-upgradeable/token/TRC721/TRC721Upgradeable.sol"; + +-contract MyCollectible is TRC721 { ++contract MyCollectible is TRC721Upgradeable { +``` + + +Interfaces and libraries are not included in the Upgradeable package, but are instead imported from the `@openzeppelin/tron-contracts` peer package. + + +Constructors are replaced by internal initializer functions following the naming convention `__{ContractName}_init`. Since these are internal, you must always define your own public initializer function and call the parent initializer of the contract you extend. + +```diff +- constructor() TRC721("MyCollectible", "MCO") public { ++ function initialize() initializer public { ++ __TRC721_init("MyCollectible", "MCO"); + } +``` + + +Use with multiple inheritance requires special attention. See the section below titled [Multiple Inheritance](#multiple-inheritance). + + +### Deploying behind a proxy + +Once this contract is set up and compiled, deploy it **behind a proxy** and call your `initialize` function exactly once. The implementation contract is never used directly. The flow is: + +1. Deploy the implementation contract (`MyCollectible`). +2. Deploy a proxy — for example a `TRC1967Proxy` from `@openzeppelin/tron-contracts` — pointing at the implementation, passing the ABI-encoded `initialize(...)` call as its initialization data. This runs `initialize` in the proxy's storage context, atomically with deployment. +3. Interact with the **proxy** address using the implementation's ABI. + +Because the EVM Upgrades Plugins do not support Tron, perform these steps with a Tron deployment toolchain such as [TronBox](https://github.com/tronprotocol/tronbox) or [`@openzeppelin/hardhat-tron`](https://github.com/OpenZeppelin/hardhat-tron) (which routes deploys through TronWeb). For UUPS proxies, the upgrade authorization (`_authorizeUpgrade`) lives in the implementation, so wire up its access control inside `initialize`. + + +Never call `initialize` on the implementation contract directly, and do not leave the implementation uninitialized in a way that lets someone else initialize it. Protect it by calling `_disableInitializers()` in the implementation's constructor — this is one of the few places a constructor is still used in an upgradeable contract. + + +## Further Notes + +### Multiple Inheritance + +Initializer functions are not linearized by the compiler like constructors. Because of this, each `__{ContractName}_init` function embeds the linearized calls to all parent initializers. As a consequence, calling two of these `init` functions can potentially initialize the same contract twice. + +The function `__{ContractName}_init_unchained` found in every contract is the initializer function minus the calls to parent initializers, and can be used to avoid the double initialization problem, but doing this manually is not recommended. + +### Namespaced Storage + +You may notice that contracts use a struct with the `@custom:storage-location erc7201:` annotation to store the contract's state variables. This follows the [ERC-7201: Namespaced Storage Layout](https://eips.ethereum.org/EIPS/eip-7201) pattern, where each contract has its own storage layout in a namespace that is separate from other contracts in the inheritance chain. + +Without namespaced storage, it isn't safe to simply add a state variable because it "shifts down" all of the state variables below in the inheritance chain. This makes the storage layouts incompatible, as explained in [Backwards Compatibility](/tron-contracts/backwards-compatibility). + +The namespaced storage pattern used in the Upgradeable package allows us to freely add new state variables in the future without compromising the storage compatibility with existing deployments. It also allows changing the inheritance order with no impact on the resulting storage layout, as long as all inherited contracts use namespaced storage. diff --git a/content/tron-contracts/utilities.mdx b/content/tron-contracts/utilities.mdx new file mode 100644 index 00000000..f95e212e --- /dev/null +++ b/content/tron-contracts/utilities.mdx @@ -0,0 +1,663 @@ +--- +title: "Utilities" +--- + +The OpenZeppelin Contracts provide a ton of useful utilities that you can use in your project. For a complete list, check out the API Reference. +Here are some of the more popular ones. + +## Cryptography + +### Checking Signatures On-Chain + +At a high level, signatures are a set of cryptographic algorithms that allow for a _signer_ to prove himself as the owner of a _private key_ used to authorize a piece of information (generally a transaction). Natively, the EVM supports the Elliptic Curve Digital Signature Algorithm ([ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm)) using the secp256k1 curve, however other signature algorithms such as P256 and RSA are supported. + +#### Ethereum Signatures (secp256k1) + +`ECDSA` provides functions for recovering and managing Ethereum account ECDSA signatures. These are often generated via [`web3.eth.sign`](https://web3js.readthedocs.io/en/v1.7.3/web3-eth.html#sign), and form a 65-byte array (of type `bytes` in Solidity) arranged the following way: `[[v (1)], [r (32)], [s (32)]]`. + +The data signer can be recovered with `ECDSA.recover`, and its address compared to verify the signature. Native TRON wallets (e.g. TronWeb's `signMessageV2` and TronLink) will hash the data to sign and add the prefix `\x19TRON Signed Message:\n`, so when attempting to recover the signer of a TRON signed message hash, you'll want to use `toTronSignedMessageHash`. + +```solidity +using ECDSA for bytes32; +using MessageHashUtils for bytes32; + +function _verify(bytes32 data, bytes memory signature, address account) internal pure returns (bool) { + return data + .toTronSignedMessageHash() + .recover(signature) == account; +} +``` + + +Getting signature verification right is not trivial: make sure you fully read and understand `MessageHashUtils`'s and `ECDSA`'s documentation. + + +#### P256 Signatures (secp256r1) + +P256, also known as secp256r1, is one of the most used signature schemes. P256 signatures are standardized by the National Institute of Standards and Technology (NIST) and they are widely available in consumer hardware and software. + +These signatures are different from regular Ethereum Signatures (secp256k1) in that they use a different elliptic curve to perform operations but have similar security guarantees. + +```solidity +using P256 for bytes32; + +function _verify( + bytes32 data, + bytes32 r, + bytes32 s, + bytes32 qx, + bytes32 qy +) internal view returns (bool) { + return data.verify(r, s, qx, qy); +} +``` + +By default, the `verify` function will try calling the [RIP-7212](https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md) precompile at address `0x100` and will fallback to an implementation in Solidity if not available. We encourage you to use `verifyNative` if you know the precompile is available on the chain you're working on and on any other chain on which you intend to use the same bytecode in the future. In case of any doubts regarding the implementation roadmap of the native precompile `P256` of potential future target chains, please consider using `verifySolidity`. + +```solidity +using P256 for bytes32; + +function _verify( + bytes32 data, + bytes32 r, + bytes32 s, + bytes32 qx, + bytes32 qy +) internal view returns (bool) { + // Will only call the precompile at address(0x100) + return data.verifyNative(r, s, qx, qy); +} +``` + + +The P256 library only allows for `s` values in the lower order of the curve (i.e. `s <= N/2`) to prevent malleability. In case your tooling produces signatures in both sides of the curve, consider flipping the `s` value to keep compatibility. + + +#### RSA + +RSA is a public-key cryptosystem that was popularized by corporate and governmental public key infrastructures ([PKIs](https://en.wikipedia.org/wiki/Public_key_infrastructure)) and [DNSSEC](https://en.wikipedia.org/wiki/Domain_Name_System_Security_Extensions). + +This cryptosystem consists of using a private key that's the product of 2 large prime numbers. The message is signed by applying a modular exponentiation to its hash (commonly SHA256), where both the exponent and modulus compose the public key of the signer. + +RSA signatures are known for being less efficient than elliptic curve signatures given the size of the keys, which are big compared to ECDSA keys with the same security level. Using plain RSA is considered unsafe, this is why the implementation uses the `EMSA-PKCS1-v1_5` encoding method from [RFC8017](https://datatracker.ietf.org/doc/html/rfc8017) to include padding to the signature. + +To verify a signature using RSA, you can leverage the `RSA` library that exposes a method for verifying RSA with the PKCS 1.5 standard: + +```solidity +using RSA for bytes32; + +function _verify( + bytes32 data, + bytes memory signature, + bytes memory e, + bytes memory n +) internal pure returns (bool) { + return data.pkcs1Sha256(signature, e, n); +} +``` + + +Always use keys of at least 2048 bits. Additionally, be aware that PKCS#1 v1.5 allows for replayability due to the possibility of arbitrary optional parameters. To prevent replay attacks, consider including an onchain nonce or unique identifier in the message. + + +### Signature Verification + +The `SignatureChecker` library provides a unified interface for verifying signatures from different sources. It seamlessly supports: + +- ECDSA signatures from externally owned accounts (EOAs) +- ERC-1271 signatures from smart contract wallets like Argent and Safe Wallet +- ERC-7913 signatures from keys that don't have their own Ethereum address + +This allows developers to write signature verification code once and have it work across all these different signature types. + +#### Basic Signature Verification + +For standard signature verification that supports both EOAs and ERC-1271 contracts: + +```solidity +using SignatureChecker for address; + +function _verifySignature(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) { + return SignatureChecker.isValidSignatureNow(signer, hash, signature); +} +``` + +The library automatically detects whether the signer is an EOA or a contract and uses the appropriate verification method. + +#### ERC-1271 Contract Signatures + +For smart contract wallets that implement ERC-1271, you can explicitly use: + +```solidity +function _verifyContractSignature(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) { + return SignatureChecker.isValidERC1271SignatureNow(signer, hash, signature); +} +``` + +#### ERC-7913 Extended Signatures + +ERC-7913 extends signature verification to support keys that don't have their own Ethereum address. This is useful for integrating non-Ethereum cryptographic curves, hardware devices, or other identity systems. + +A signer is represented as a `bytes` object that concatenates a verifier address and a key: `verifier || key`. + +```solidity +function _verifyTRC7913Signature(bytes memory signer, bytes32 hash, bytes memory signature) internal view returns (bool) { + return SignatureChecker.isValidSignatureNow(signer, hash, signature); +} +``` + +The verification process works as follows: + +- If `signer.length < 20`: verification fails +- If `signer.length == 20`: verification is done using standard signature checking +- Otherwise: verification is done using an ERC-7913 verifier + +#### Batch Verification + +For verifying multiple ERC-7913 signatures at once: + +```solidity +function _verifyMultipleSignatures( + bytes32 hash, + bytes[] memory signers, + bytes[] memory signatures +) internal view returns (bool) { + return SignatureChecker.areValidSignaturesNow(hash, signers, signatures); +} +``` + +This function will reject inputs that contain duplicated signers. Sorting the signers by their `keccak256` hash is recommended to minimize the gas cost. + +This unified approach allows smart contracts to accept signatures from any supported source without needing to implement different verification logic for each type. + +### Verifying Merkle Proofs + +Developers can build a Merkle Tree off-chain, which allows for verifying that an element (leaf) is part of a set by using a Merkle Proof. This technique is widely used for creating whitelists (e.g., for airdrops) and other advanced use cases. + + +OpenZeppelin Contracts provides a [JavaScript library](https://github.com/OpenZeppelin/merkle-tree) for building trees off-chain and generating proofs. + + +`MerkleProof` provides: + +- `verify` - can prove that some value is part of a [Merkle tree](https://en.wikipedia.org/wiki/Merkle_tree). + +- `multiProofVerify` - can prove multiple values are part of a Merkle tree. + +For an on-chain Merkle Tree, see the `MerkleTree` library. + +## Introspection + +In Solidity, it's frequently helpful to know whether or not a contract supports an interface you'd like to use. ERC-165 is a standard that enables runtime interface detection. Contracts provide helpers both for implementing ERC-165 in your contracts and querying other contracts: + +- `IERC165` — this is the ERC-165 interface that defines `supportsInterface`. When implementing ERC-165, you'll conform to this interface. +- `ERC165` — inherit this contract if you'd like to support interface detection using a lookup table in contract storage. You can register interfaces using `_registerInterface(bytes4)`: check out example usage as part of the TRC-721 implementation. +- `ERC165Checker` — ERC165Checker simplifies the process of checking whether or not a contract supports an interface you care about. +- include with `using ERC165Checker for address;` +- `myAddress._supportsInterface(bytes4)` +- `myAddress._supportsAllInterfaces(bytes4[])` + +```solidity +contract MyContract { + using ERC165Checker for address; + + bytes4 private InterfaceId_TRC721 = 0x80ac58cd; + + /** + * @dev transfer a TRC-721 token from this contract to someone else + */ + function transferTRC721( + address token, + address to, + uint256 tokenId + ) + public + { + require(token.supportsInterface(InterfaceId_TRC721), "IS_NOT_721_TOKEN"); + ITRC721(token).transferFrom(address(this), to, tokenId); + } +} +``` + +## Math + +Although Solidity already provides math operators (i.e. `+`, `-`, etc.), Contracts includes `Math`; a set of utilities for dealing with mathematical operators, with support for extra operations (e.g., `average`) and `SignedMath`; a library specialized in signed math operations. + +Include these contracts with `using Math for uint256` or `using SignedMath for int256` and then use their functions in your code: + +```solidity +contract MyContract { + using Math for uint256; + using SignedMath for int256; + + function tryOperations(uint256 a, uint256 b) internal pure { + (bool succeededAdd, uint256 resultAdd) = x.tryAdd(y); + (bool succeededSub, uint256 resultSub) = x.trySub(y); + (bool succeededMul, uint256 resultMul) = x.tryMul(y); + (bool succeededDiv, uint256 resultDiv) = x.tryDiv(y); + // ... + } + + function unsignedAverage(int256 a, int256 b) { + int256 avg = a.average(b); + // ... + } +} +``` + +Easy! + + +While working with different data types that might require casting, you can use `SafeCast` for type casting with added overflow checks. + + +## Structures + +Some use cases require more powerful data structures than the arrays and mappings offered natively in Solidity. These contracts provide libraries for enhanced data structure management: + +- `BitMaps`: Store packed booleans in storage. +- `Checkpoints`: Checkpoint values with built-in lookups. +- `DoubleEndedQueue`: Store items in a queue with `pop()` and `queue()` constant time operations. +- `EnumerableSet`: A [set](https://en.wikipedia.org/wiki/Set_(abstract_data_type)) with enumeration capabilities. +- `EnumerableMap`: A `mapping` variant with enumeration capabilities. +- `MerkleTree`: An on-chain [Merkle Tree](https://wikipedia.org/wiki/Merkle_Tree) with helper functions. +- `Heap`: A [binary heap](https://en.wikipedia.org/wiki/Binary_heap) to store elements with priority defined by a comparator function. + +The `Enumerable*` structures are similar to mappings in that they store and remove elements in constant time and don't allow for repeated entries, but they also support _enumeration_, which means you can easily query all stored entries both on and off-chain. + +### Building a Merkle Tree + +Building an on-chain Merkle Tree allows developers to keep track of the history of roots in a decentralized manner. For these cases, the `MerkleTree` includes a predefined structure with functions to manipulate the tree (e.g. pushing values or resetting the tree). + +The Merkle Tree does not keep track of the roots intentionally, so that developers can choose their tracking mechanism. Setting up and using a Merkle Tree in Solidity is as simple as follows: + + +Functions are exposed without access control for demonstration purposes + + +```solidity +using MerkleTree for MerkleTree.Bytes32PushTree; +MerkleTree.Bytes32PushTree private _tree; + +function setup(uint8 _depth, bytes32 _zero) public /* onlyOwner */ { + root = _tree.setup(_depth, _zero); +} + +function push(bytes32 leaf) public /* onlyOwner */ { + (uint256 leafIndex, bytes32 currentRoot) = _tree.push(leaf); + // Store the new root. +} +``` + +The library also supports custom hashing functions, which can be passed as an extra parameter to the `push` and `setup` functions. + +Using custom hashing functions is a sensitive operation. After setup, it requires continuing to use the same hashing function for every new value pushed to the tree to avoid corrupting the tree. For this reason, it's a good practice to keep your hashing function static in your implementation contract as follows: + +```solidity +using MerkleTree for MerkleTree.Bytes32PushTree; +MerkleTree.Bytes32PushTree private _tree; + +function setup(uint8 _depth, bytes32 _zero) public /* onlyOwner */ { + root = _tree.setup(_depth, _zero, _hashFn); +} + +function push(bytes32 leaf) public /* onlyOwner */ { + (uint256 leafIndex, bytes32 currentRoot) = _tree.push(leaf, _hashFn); + // Store the new root. +} + +function _hashFn(bytes32 a, bytes32 b) internal view returns(bytes32) { + // Custom hash function implementation + // Kept as an internal implementation detail to + // guarantee the same function is always used +} +``` + +### Using a Heap + +A [binary heap](https://en.wikipedia.org/wiki/Binary_heap) is a data structure that always stores the most important element at its peak and it can be used as a priority queue. + +To define what is most important in a heap, these frequently take comparator functions that tell the binary heap whether a value has more relevance than another. + +OpenZeppelin Contracts implements a Heap data structure with the properties of a binary heap. The heap uses the `lt` function by default but allows to customize its comparator. + +When using a custom comparator, it's recommended to wrap your function to avoid the possibility of mistakenly using a different comparator function: + +```solidity +function pop(Uint256Heap storage self) internal returns (uint256) { + return pop(self, Comparators.gt); +} + +function insert(Uint256Heap storage self, uint256 value) internal { + insert(self, value, Comparators.gt); +} + +function replace(Uint256Heap storage self, uint256 newValue) internal returns (uint256) { + return replace(self, newValue, Comparators.gt); +} +``` + +## Misc + +### Packing + +The storage in the EVM is shaped in chunks of 32 bytes, each of these chunks is known as a _slot_, and can hold multiple values together as long as these values don't exceed 32 bytes. This property allows for a technique known as _packing_--placing values together in a single storage slot to reduce the costs associated with reading and writing these values. + +Commonly, developers pack values using structs that place values together so they fit better in storage. However, this approach requires loading such struct from either calldata or memory. Although sometimes necessary, it may be useful to pack values in a single slot and treat it as a packed value without involving calldata or memory. + +The `Packing` library is a set of utilities for packing values that fit in 32 bytes. The library includes 3 main functionalities: + +- Packing 2 `bytesXX` values +- Extracting a packed `bytesXX` value from a `bytesYY` +- Replacing a packed `bytesXX` value from a `bytesYY` + +With these primitives, one can build custom functions to create custom packed types. For example, suppose you need to pack an `address` of 20 bytes with a `bytes4` selector and an `uint64` time period: + +```solidity +function _pack(address account, bytes4 selector, uint64 period) external pure returns (bytes32) { + bytes12 subpack = Packing.pack_4_8(selector, bytes8(period)); + return Packing.pack_20_12(bytes20(account), subpack); +} + +function _unpack(bytes32 pack) external pure returns (address, bytes4, uint64) { + return ( + address(Packing.extract_32_20(pack, 0)), + Packing.extract_32_4(pack, 20), + uint64(Packing.extract_32_8(pack, 24)) + ); +} +``` + +### Storage Slots + +Solidity allocates a storage pointer for each variable declared in a contract. However, there are cases when it's required to access storage pointers that can't be derived by using regular Solidity. +For those cases, the `StorageSlot` library allows for manipulating storage slots directly. + +```solidity +bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + +function _getImplementation() internal view returns (address) { + return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; +} + +function _setImplementation(address newImplementation) internal { + require(newImplementation.code.length > 0); + StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; +} +``` + +The `TransientSlot` library supports transient storage through user defined value types ([UDVTs](https://docs.soliditylang.org/en/latest/types.html#user-defined-value-types)), which enables the same value types as in Solidity. + +```solidity +bytes32 internal constant _LOCK_SLOT = 0xf4678858b2b588224636b8522b729e7722d32fc491da849ed75b3fdf3c84f542; + +function _getTransientLock() internal view returns (bool) { + return _LOCK_SLOT.asBoolean().tload(); +} + +function _setTransientLock(bool lock) internal { + _LOCK_SLOT.asBoolean().tstore(lock); +} +``` + + +Manipulating storage slots directly is an advanced practice. Developers MUST make sure that the storage pointer is not colliding with other variables. + + +One of the most common use cases for writing directly to storage slots is ERC-7201 for namespaced storage, which is guaranteed to not collide with other storage slots derived by Solidity. + +Users can leverage this standard using the `SlotDerivation` library. + +```solidity +using SlotDerivation for bytes32; +string private constant _NAMESPACE = "" // eg. example.main + +function erc7201Pointer() internal view returns (bytes32) { + return _NAMESPACE.erc7201Slot(); +} +``` + +### Base64 + +`Base64` util allows you to transform `bytes32` data into its Base64 `string` representation. + +This is especially useful for building URL-safe tokenURIs for both `TRC-721` or `TRC-1155`. This library provides a clever way to serve URL-safe [Data URI](https://developer.mozilla.org/docs/Web/HTTP/Basics_of_HTTP/Data_URIs/) compliant strings to serve on-chain data structures. + +Here is an example to send JSON Metadata through a Base64 Data URI using a TRC-721: + +```solidity +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import {TRC721} from "@openzeppelin/tron-contracts/token/TRC721/TRC721.sol"; +import {Strings} from "@openzeppelin/tron-contracts/utils/Strings.sol"; +import {Base64} from "@openzeppelin/tron-contracts/utils/Base64.sol"; + +contract Base64NFT is TRC721 { + using Strings for uint256; + + constructor() TRC721("Base64NFT", "MTK") {} + + // ... + + function tokenURI(uint256 tokenId) public pure override returns (string memory) { + // Equivalent to: + // { + // "name": "Base64NFT #1", + // // Replace with extra TRC-721 Metadata properties + // } + // prettier-ignore + string memory dataURI = string.concat("{\"name\": \"Base64NFT #", tokenId.toString(), "\"}"); + + return string.concat("data:application/json;base64,", Base64.encode(bytes(dataURI))); + } +} +``` + +### Multicall + +The `Multicall` abstract contract comes with a `multicall` function that bundles together multiple calls in a single external call. With it, external accounts may perform atomic operations comprising several function calls. This is not only useful for EOAs to make multiple calls in a single transaction, it's also a way to revert a previous call if a later one fails. + +Consider this dummy contract: + +```solidity +// contracts/Box.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Multicall} from "@openzeppelin/tron-contracts/utils/Multicall.sol"; + +contract Box is Multicall { + function foo() public { + // ... + } + + function bar() public { + // ... + } +} +``` + +This is how to call the `multicall` function using Ethers.js, allowing `foo` and `bar` to be called in a single transaction: + +```javascript +// scripts/foobar.js + +const instance = await ethers.deployContract("Box"); + +await instance.multicall([ + instance.interface.encodeFunctionData("foo"), + instance.interface.encodeFunctionData("bar") +]); +``` + +### Low-level Calls + +The `LowLevelCall` library provides low-level external calls with fixed-size return data handling, protecting against return bombing attacks where callees allocate excessive memory. + +The library efficiently handles return data up to 64 bytes, allowing you to ignore it entirely or extract 1-2 `bytes32` values: + +```solidity +using LowLevelCall for address; + +function example(address target, bytes memory data) internal { + bool success; + bytes32 result1; + bytes32 result2; + + // Ignore return data + success = target.callNoReturn(data); + + // Extract single 32-byte value + (success, result1, ) = target.callReturn64Bytes(data); + + // Extract two 32-byte values + (success, result1, result2) = target.callReturn64Bytes(data); +} +``` + +You can also check return data size before processing: + +```solidity +function checkReturnSize(address target, bytes memory data) internal returns (uint256 value, uint256 otherValue) { + (bool success, bytes32 result1, bytes32 result2) = target.callReturn64Bytes(data); + + if (!success || LowLevelCall.returnDataSize() < 32) { + return (0, 0); + } else if (LowLevelCall.returnDataSize() < 64) { + return (uint256(result1), 0); + } else { + return (uint256(result1), uint256(result2)); + } +} +``` + +### Memory + +The `Memory` library provides functions for advanced use cases that require granular memory management. A common use case is to avoid unnecessary memory expansion costs when performing repeated operations that allocate memory in a loop. Consider the following example: + +```solidity +function processMultipleItems(uint256[] memory items) internal { + for (uint256 i = 0; i < items.length; i++) { + bytes memory tempData = abi.encode(items[i], block.timestamp); + // Process tempData... + } +} +``` + +Note that each iteration allocates new memory for `tempData`, causing the memory to expand continuously. This can be optimized by resetting the memory pointer between iterations: + +```solidity +function processMultipleItems(uint256[] memory items) internal { + Memory.Pointer ptr = Memory.getFreeMemoryPointer(); // Cache pointer + for (uint256 i = 0; i < items.length; i++) { + bytes memory tempData = abi.encode(items[i], block.timestamp); + // Process tempData... + Memory.unsafeSetFreeMemoryPointer(ptr); // Reset pointer for reuse + } +} +``` + +This way, memory allocated for `tempData` in each iteration is reused, significantly reducing memory expansion costs when processing many items. + + +Only use these functions after carefully confirming they're necessary. By default, Solidity handles memory safely. Using this library without understanding memory layout and safety may be dangerous. See the [memory layout](https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_memory.html) and [memory safety](https://docs.soliditylang.org/en/v0.8.20/assembly.html#memory-safety) documentation for details. + + +### Historical Block Hashes + +`Blockhash` provides access to historical block hashes through the native `BLOCKHASH` opcode, which exposes the hashes of the most recent 256 blocks. + +The library is a thin wrapper around the opcode. It handles edge cases gracefully by returning zero for the current block, future blocks, or blocks older than 256, matching the VM's native behavior. Note that TVM does not implement [EIP-2935](https://eips.ethereum.org/EIPS/eip-2935) history storage, so access cannot be extended beyond the 256-block window. + +```solidity +contract L1Inbox { + using Blockhash for uint256; + + function verifyBlockHash(uint256 blockNumber, bytes32 expectedHash) public view returns (bool) { + return blockNumber.blockHash() == expectedHash; + } +} +``` + + +Only the 256 most recent block hashes are available. Any query for an older block, the current block, or a future block returns `bytes32(0)`. Treat a zero result as "unavailable" rather than a valid hash. + + +### Time + +The `Time` library provides helpers for manipulating time-related objects in a type-safe manner. It uses `uint48` for timepoints and `uint32` for durations, helping to reduce gas costs while providing adequate precision. + +One of its key features is the `Delay` type, which represents a duration that can automatically change its value at a specified point in the future while maintaining delay guarantees. For example, when reducing a delay value (e.g., from 7 days to 1 day), the change only takes effect after the difference between the old and new delay (i.e. a 6 days) or a minimum setback period, preventing an attacker who gains admin access from immediately reducing security timeouts and executing sensitive operations. This is particularly useful for governance and security mechanisms where timelock periods need to be enforced. + +Consider this example for using and safely updating Delays: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {Time} from "@openzeppelin/tron-contracts/utils/types/Time.sol"; + +contract MyDelayedContract { + using Time for *; + + Time.Delay private _delay; + + constructor() { + _delay = Time.toDelay(3 days); + } + + function schedule(bytes32 operationId) external { + // Get the current `_delay` value, respecting any pending delay changes if they've taken effect + uint32 currentDelay = _delay.get(); + uint48 executionTime = Time.timestamp() + currentDelay; + + // ... schedule the operation at `executionTime` + } + + function execute(bytes32 operationId) external { + uint48 executionTime = getExecutionTime(operationId); + require(executionTime > 0, "Operation not scheduled"); + require(Time.timestamp() >= executionTime, "Delay not elapsed yet"); + + // ... execute the operation + } + + // Update the delay with `Time`'s safety mechanism + function updateDelay(uint32 newDelay) external { + (Time.Delay updatedDelay, uint48 effect) = _delay.withUpdate( + newDelay, // The new delay value + 5 days // Minimum setback if reducing the delay + ); + + _delay = updatedDelay; + + // ... emit events + } + + // Get complete delay details including pending changes + function getDelayDetails() external view returns ( + uint32 currentValue, // The current delay value + uint32 pendingValue, // The pending delay value + uint48 effectTime // The timepoint when the pending delay change takes effect + ) { + return _delay.getFull(); + } +} +``` + +This pattern is used extensively in OpenZeppelin's AccessManager for implementing secure time-based access control. For example, when changing an admin delay: + +```solidity +// From AccessManager.sol +function _setTargetAdminDelay(address target, uint32 newDelay) internal virtual { + uint48 effect; + (_targets[target].adminDelay, effect) = _targets[target].adminDelay.withUpdate( + newDelay, + minSetback() + ); + + emit TargetAdminDelayUpdated(target, newDelay, effect); +} +``` diff --git a/public/tron/access-control-multiple.svg b/public/tron/access-control-multiple.svg new file mode 100644 index 00000000..0314e09e --- /dev/null +++ b/public/tron/access-control-multiple.svg @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/tron/access-manager-functions.svg b/public/tron/access-manager-functions.svg new file mode 100644 index 00000000..dbbf0417 --- /dev/null +++ b/public/tron/access-manager-functions.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/tron/access-manager.svg b/public/tron/access-manager.svg new file mode 100644 index 00000000..12f91bae --- /dev/null +++ b/public/tron/access-manager.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/tron/erc4626-attack-3a.png b/public/tron/erc4626-attack-3a.png new file mode 100644 index 00000000..4cb52237 Binary files /dev/null and b/public/tron/erc4626-attack-3a.png differ diff --git a/public/tron/erc4626-attack-3b.png b/public/tron/erc4626-attack-3b.png new file mode 100644 index 00000000..3dc5256b Binary files /dev/null and b/public/tron/erc4626-attack-3b.png differ diff --git a/public/tron/erc4626-attack-6.png b/public/tron/erc4626-attack-6.png new file mode 100644 index 00000000..1587fb5c Binary files /dev/null and b/public/tron/erc4626-attack-6.png differ diff --git a/public/tron/erc4626-attack.png b/public/tron/erc4626-attack.png new file mode 100644 index 00000000..dc059b22 Binary files /dev/null and b/public/tron/erc4626-attack.png differ diff --git a/public/tron/erc4626-deposit.png b/public/tron/erc4626-deposit.png new file mode 100644 index 00000000..b6c75e67 Binary files /dev/null and b/public/tron/erc4626-deposit.png differ diff --git a/public/tron/erc4626-mint.png b/public/tron/erc4626-mint.png new file mode 100644 index 00000000..f89ab900 Binary files /dev/null and b/public/tron/erc4626-mint.png differ diff --git a/public/tron/erc4626-rate-linear.png b/public/tron/erc4626-rate-linear.png new file mode 100644 index 00000000..09e8045e Binary files /dev/null and b/public/tron/erc4626-rate-linear.png differ diff --git a/public/tron/erc4626-rate-loglog.png b/public/tron/erc4626-rate-loglog.png new file mode 100644 index 00000000..4eb19efe Binary files /dev/null and b/public/tron/erc4626-rate-loglog.png differ diff --git a/public/tron/erc4626-rate-loglogext.png b/public/tron/erc4626-rate-loglogext.png new file mode 100644 index 00000000..127bc7f2 Binary files /dev/null and b/public/tron/erc4626-rate-loglogext.png differ diff --git a/public/tron/tally-exec.png b/public/tron/tally-exec.png new file mode 100644 index 00000000..e24a145d Binary files /dev/null and b/public/tron/tally-exec.png differ diff --git a/public/tron/tally-vote.png b/public/tron/tally-vote.png new file mode 100644 index 00000000..7d270fce Binary files /dev/null and b/public/tron/tally-vote.png differ diff --git a/src/app/page.tsx b/src/app/page.tsx index e1db814e..1b08c442 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -25,6 +25,7 @@ import { StellarIcon, SuiIcon, TransactionProposalIcon, + TronIcon, UniswapIcon, ZamaIcon, } from "@/components/icons"; @@ -113,7 +114,7 @@ export default function HomePage() {
- +4 + +5
@@ -222,6 +223,14 @@ export default function HomePage() { glowColor="sui" /> + } + title="Tron" + description="Build secure Solidity smart contracts on Tron's TVM with the TRC token standards" + glowColor="tron" + /> + } diff --git a/src/components/home-cards.tsx b/src/components/home-cards.tsx index 1b6be5fa..c9b587d3 100644 --- a/src/components/home-cards.tsx +++ b/src/components/home-cards.tsx @@ -160,6 +160,7 @@ function EcosystemCard({ polkadot: "before:bg-gradient-to-r before:from-[#e6007a] before:to-[#ff4081]", sui: "before:bg-gradient-to-r before:from-[#7dd3fc] before:to-[#0284c7]", + tron: "before:bg-gradient-to-r before:from-[#EF0027] before:to-[#FF060A]", uniswap: "before:bg-gradient-to-r before:from-[#e6007a] before:to-[#FC75FE]", zama: "before:bg-gradient-to-r before:from-[#FFD205] before:to-[#FFD205]", diff --git a/src/components/icons/index.ts b/src/components/icons/index.ts index 6581ba71..f93396cd 100644 --- a/src/components/icons/index.ts +++ b/src/components/icons/index.ts @@ -15,5 +15,6 @@ export { StarknetIcon } from "./starknet-icon"; export { StellarIcon } from "./stellar-icon"; export { SuiIcon } from "./sui-icon"; export { TransactionProposalIcon } from "./transaction-proposal-icon"; +export { TronIcon } from "./tron-icon"; export { UniswapIcon } from "./uniswap-icon"; export { ZamaIcon } from "./zama-icon"; diff --git a/src/components/icons/tron-icon.tsx b/src/components/icons/tron-icon.tsx new file mode 100644 index 00000000..3e2d12a1 --- /dev/null +++ b/src/components/icons/tron-icon.tsx @@ -0,0 +1,17 @@ +export function TronIcon({ className }: { className: string }) { + return ( + + TRON Icon + + + ); +} diff --git a/src/components/layout/docs-layout-client.tsx b/src/components/layout/docs-layout-client.tsx index 238746e7..953b3675 100644 --- a/src/components/layout/docs-layout-client.tsx +++ b/src/components/layout/docs-layout-client.tsx @@ -12,6 +12,7 @@ import { StarknetIcon, StellarIcon, SuiIcon, + TronIcon, UniswapIcon, ZamaIcon, } from "@/components/icons"; @@ -153,6 +154,11 @@ export function DocsLayoutClient({ children }: DocsLayoutClientProps) { url: "/contracts-sui", icon: , }, + { + title: "Tron", + url: "/tron-contracts", + icon: , + }, { title: "Stellar", url: "/stellar-contracts", diff --git a/src/hooks/use-navigation-tree.ts b/src/hooks/use-navigation-tree.ts index 50b8c9c3..9b80b85d 100644 --- a/src/hooks/use-navigation-tree.ts +++ b/src/hooks/use-navigation-tree.ts @@ -13,6 +13,7 @@ import { starknetTree, stellarTree, suiTree, + tronTree, uniswapTree, zamaTree, } from "@/navigation"; @@ -36,6 +37,8 @@ export function useNavigationTree() { sessionStorage.setItem("lastEcosystem", "polkadot"); } else if (pathname.startsWith("/contracts-sui")) { sessionStorage.setItem("lastEcosystem", "sui"); + } else if (pathname.startsWith("/tron-contracts")) { + sessionStorage.setItem("lastEcosystem", "tron"); } else if (pathname.startsWith("/contracts-stylus")) { sessionStorage.setItem("lastEcosystem", "contracts-stylus"); } else if (pathname.startsWith("/contracts-compact")) { @@ -76,6 +79,8 @@ export function useNavigationTree() { return starknetTree; } else if (pathname.startsWith("/contracts-sui")) { return suiTree; + } else if (pathname.startsWith("/tron-contracts")) { + return tronTree; } else if (pathname.startsWith("/stellar-contracts")) { return stellarTree; } else if (pathname.startsWith("/contracts-compact")) { diff --git a/src/lib/ecosystem-detection.ts b/src/lib/ecosystem-detection.ts index 6a8667c6..012a459b 100644 --- a/src/lib/ecosystem-detection.ts +++ b/src/lib/ecosystem-detection.ts @@ -8,6 +8,7 @@ import { starknetTree, stellarTree, suiTree, + tronTree, uniswapTree, zamaTree, } from "@/navigation"; @@ -27,6 +28,7 @@ export type EcosystemKey = | "midnight" | "sui" | "starknet" + | "tron" | "uniswap"; const TREES: Array<{ key: EcosystemKey; tree: NavigationTree }> = [ @@ -38,6 +40,7 @@ const TREES: Array<{ key: EcosystemKey; tree: NavigationTree }> = [ { key: "midnight", tree: midnightTree }, { key: "sui", tree: suiTree }, { key: "starknet", tree: starknetTree }, + { key: "tron", tree: tronTree }, { key: "uniswap", tree: uniswapTree }, ]; diff --git a/src/navigation/index.ts b/src/navigation/index.ts index 541cf948..0b3fbe6b 100644 --- a/src/navigation/index.ts +++ b/src/navigation/index.ts @@ -7,6 +7,7 @@ import polkadotData from "./polkadot.json"; import starknetData from "./starknet"; import stellarData from "./stellar.json"; import suiData from "./sui"; +import tronData from "./tron.json"; import type { NavigationNode, NavigationTree } from "./types"; import uniswapData from "./uniswap.json"; import zamaData from "./zama.json"; @@ -23,6 +24,7 @@ const stellar = mergeDeveloperLibraries(stellarData as NavigationNode[]); const midnight = mergeDeveloperLibraries(midnightData as NavigationNode[]); const starknet = starknetData as NavigationNode[]; const sui = suiData as NavigationNode[]; +const tron = tronData as NavigationNode[]; const polkadot = mergeDeveloperLibraries(polkadotData as NavigationNode[]); const zama = mergeDeveloperLibraries(zamaData as NavigationNode[]); const uniswap = uniswapData as NavigationNode[]; @@ -59,6 +61,11 @@ export const suiTree: NavigationTree = { children: sui, }; +export const tronTree: NavigationTree = { + name: "Tron", + children: tron, +}; + export const zamaTree: NavigationTree = { name: "Zama FHEVM", children: zama, diff --git a/src/navigation/tron.json b/src/navigation/tron.json new file mode 100644 index 00000000..a24e8540 --- /dev/null +++ b/src/navigation/tron.json @@ -0,0 +1,107 @@ +[ + { + "type": "page", + "name": "Overview", + "url": "/tron-contracts" + }, + { + "type": "page", + "name": "TVM Differences", + "url": "/tron-contracts/tvm-differences" + }, + { + "type": "separator", + "name": "Guides" + }, + { + "type": "page", + "name": "Extending Contracts", + "url": "/tron-contracts/extending-contracts" + }, + { + "type": "page", + "name": "Using with Upgrades", + "url": "/tron-contracts/upgradeable" + }, + { + "type": "page", + "name": "Backwards Compatibility", + "url": "/tron-contracts/backwards-compatibility" + }, + { + "type": "page", + "name": "Access Control", + "url": "/tron-contracts/access-control" + }, + { + "type": "separator", + "name": "Tokens" + }, + { + "type": "folder", + "name": "Tokens", + "defaultOpen": true, + "index": { + "type": "page", + "name": "Overview", + "url": "/tron-contracts/tokens" + }, + "children": [ + { + "type": "folder", + "name": "TRC-20", + "index": { + "type": "page", + "name": "TRC-20", + "url": "/tron-contracts/trc20" + }, + "children": [ + { + "type": "page", + "name": "Creating Supply", + "url": "/tron-contracts/trc20-supply" + } + ] + }, + { + "type": "page", + "name": "TRC-721", + "url": "/tron-contracts/trc721" + }, + { + "type": "page", + "name": "TRC-1155", + "url": "/tron-contracts/trc1155" + }, + { + "type": "page", + "name": "TRC-4626", + "url": "/tron-contracts/trc4626" + }, + { + "type": "page", + "name": "TRC-6909", + "url": "/tron-contracts/trc6909" + } + ] + }, + { + "type": "separator", + "name": "Modules" + }, + { + "type": "page", + "name": "Governance", + "url": "/tron-contracts/governance" + }, + { + "type": "page", + "name": "Utilities", + "url": "/tron-contracts/utilities" + }, + { + "type": "page", + "name": "FAQ", + "url": "/tron-contracts/faq" + } +]