-
Notifications
You must be signed in to change notification settings - Fork 8
feat: solidity contract #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Changes from 5 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
de1c348
feat: init erc20 with hyperlane support
johnletey a6a1446
feat: introduce principal and index
johnletey 0b29956
feat: start implementation with claim functionality
johnletey c90a8c5
chore: progress commit
johnletey 362448a
feat: finalize implementation and tests
johnletey ac6e614
feat: documentation
johnletey d9935f0
chore: pin compiler version
johnletey 75f5b61
feat(contracts): improvements from M0 team (#49)
toninorair 8785241
refactor(contracts): internal M0 and Sherlock audit changes (#55)
mesozoic-technology 225fdc1
chore: resolve linter, update deps
johnletey caddd79
chore: merge main
johnletey 2ac8c47
feat: implement hyperevm specific contract
johnletey dce538f
fix: set bridge during initialization to make it upgradeable (#63)
keyleu b2d5c7d
update initialization of contracts to not have multiple initializer m…
keyleu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,9 @@ | ||
| .dollar | ||
| .idea | ||
| build | ||
| cache | ||
| node_modules | ||
| out | ||
| coverage.out | ||
| coverage.html | ||
| go.work | ||
|
|
||
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| [profile.default] | ||
| src = "src" | ||
| out = "out" | ||
| libs = ["node_modules"] | ||
| auto_detect_remappings = false | ||
| remappings = ["forge-std=node_modules/forge-std/src", "@hyperlane=node_modules/@hyperlane-xyz/core/contracts", "@openzeppelin=node_modules/@openzeppelin"] | ||
|
|
||
| # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| { | ||
| "name": "@noble-assets/usdn", | ||
| "private": true, | ||
| "devDependencies": { | ||
| "@hyperlane-xyz/core": "8.0.0", | ||
| "@openzeppelin/contracts": "5.3.0", | ||
| "@openzeppelin/contracts-upgradeable": "5.3.0", | ||
| "forge-std": "github:foundry-rs/forge-std#v1.9.7" | ||
| }, | ||
| "patchedDependencies": { | ||
| "@hyperlane-xyz/core@8.0.0": "patches/@hyperlane-xyz%2Fcore@8.0.0.patch" | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| diff --git a/contracts/client/MailboxClient.sol b/contracts/client/MailboxClient.sol | ||
| index 0b0d3642db1f2f8a2434f0a1e96438c8caf07011..12e8421805e7ff7e1f0214894a2eae02653691a9 100644 | ||
| --- a/contracts/client/MailboxClient.sol | ||
| +++ b/contracts/client/MailboxClient.sol | ||
| @@ -21,7 +21,6 @@ import {Message} from "../libs/Message.sol"; | ||
| import {PackageVersioned} from "../PackageVersioned.sol"; | ||
|
|
||
| // ============ External Imports ============ | ||
| -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; | ||
| import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; | ||
|
|
||
| abstract contract MailboxClient is OwnableUpgradeable, PackageVersioned { | ||
| @@ -43,7 +42,7 @@ abstract contract MailboxClient is OwnableUpgradeable, PackageVersioned { | ||
| // ============ Modifiers ============ | ||
| modifier onlyContract(address _contract) { | ||
| require( | ||
| - Address.isContract(_contract), | ||
| + _contract.code.length > 0, | ||
| "MailboxClient: invalid mailbox" | ||
| ); | ||
| _; | ||
| @@ -51,7 +50,7 @@ abstract contract MailboxClient is OwnableUpgradeable, PackageVersioned { | ||
|
|
||
| modifier onlyContractOrNull(address _contract) { | ||
| require( | ||
| - Address.isContract(_contract) || _contract == address(0), | ||
| + _contract.code.length > 0 || _contract == address(0), | ||
| "MailboxClient: invalid contract setting" | ||
| ); | ||
| _; | ||
| @@ -111,10 +110,9 @@ abstract contract MailboxClient is OwnableUpgradeable, PackageVersioned { | ||
| address __interchainSecurityModule, | ||
| address _owner | ||
| ) internal onlyInitializing { | ||
| - __Ownable_init(); | ||
| + __Ownable_init(_owner); | ||
| setHook(_hook); | ||
| setInterchainSecurityModule(__interchainSecurityModule); | ||
| - _transferOwnership(_owner); | ||
| } | ||
|
|
||
| function _isLatestDispatched(bytes32 id) internal view returns (bool) { | ||
| diff --git a/contracts/token/HypERC20.sol b/contracts/token/HypERC20.sol | ||
| index 6463193893c8c2d195cf24052ee71b12e76a61a0..8af0c875fedfe160de9c4dcaaccc30897750536e 100644 | ||
| --- a/contracts/token/HypERC20.sol | ||
| +++ b/contracts/token/HypERC20.sol | ||
| @@ -25,12 +25,10 @@ contract HypERC20 is ERC20Upgradeable, FungibleTokenRouter { | ||
|
|
||
| /** | ||
| * @notice Initializes the Hyperlane router, ERC20 metadata, and mints initial supply to deployer. | ||
| - * @param _totalSupply The initial supply of the token. | ||
| * @param _name The name of the token. | ||
| * @param _symbol The symbol of the token. | ||
| */ | ||
| function initialize( | ||
| - uint256 _totalSupply, | ||
| string memory _name, | ||
| string memory _symbol, | ||
| address _hook, | ||
| @@ -39,7 +37,6 @@ contract HypERC20 is ERC20Upgradeable, FungibleTokenRouter { | ||
| ) public virtual initializer { | ||
| // Initialize ERC20 metadata | ||
| __ERC20_init(_name, _symbol); | ||
| - _mint(msg.sender, _totalSupply); | ||
| _MailboxClient_initialize(_hook, _interchainSecurityModule, _owner); | ||
| } | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,214 @@ | ||
| /* | ||
| * Copyright 2025 NASD Inc. All rights reserved. | ||
| * | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| pragma solidity >=0.8.0; | ||
|
johnletey marked this conversation as resolved.
Outdated
|
||
|
|
||
| import {HypERC20} from "@hyperlane/token/HypERC20.sol"; | ||
|
|
||
| /* | ||
|
|
||
| ███╗ ██╗ ██████╗ ██████╗ ██╗ ███████╗ | ||
| ████╗ ██║██╔═══██╗██╔══██╗██║ ██╔════╝ | ||
| ██╔██╗ ██║██║ ██║██████╔╝██║ █████╗ | ||
| ██║╚██╗██║██║ ██║██╔══██╗██║ ██╔══╝ | ||
| ██║ ╚████║╚██████╔╝██████╔╝███████╗███████╗ | ||
| ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝ | ||
|
|
||
| ██████╗ ██████╗ ██╗ ██╗ █████╗ ██████╗ | ||
| ██╔══██╗██╔═══██╗██║ ██║ ██╔══██╗██╔══██╗ | ||
| ██║ ██║██║ ██║██║ ██║ ███████║██████╔╝ | ||
| ██║ ██║██║ ██║██║ ██║ ██╔══██║██╔══██╗ | ||
| ██████╔╝╚██████╔╝███████╗███████╗██║ ██║██║ ██║ | ||
| ╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ | ||
|
|
||
| */ | ||
|
|
||
| /** | ||
| * @title NobleDollar | ||
| * @author John Letey <john@noble.xyz> | ||
| * @notice ERC20 Noble Dollar. | ||
| */ | ||
| contract NobleDollar is HypERC20 { | ||
| /// @notice Thrown when a user attempts to claim yield but has no claimable yield available. | ||
| error NoClaimableYield(); | ||
|
|
||
| /// @notice Thrown when an invalid transfer to the contract is attempted. | ||
| error InvalidTransfer(); | ||
|
|
||
| /** | ||
| * @notice Emitted when the index is updated due to yield accrual. | ||
| * @param oldIndex The previous index value. | ||
| * @param newIndex The new index value. | ||
| * @param totalPrincipal The total principal amount at the time of update. | ||
| * @param yieldAccrued The amount of yield that was accrued. | ||
| */ | ||
| event IndexUpdated(uint128 oldIndex, uint128 newIndex, uint256 totalPrincipal, uint256 yieldAccrued); | ||
|
|
||
| /** | ||
| * @notice Emitted when yield is claimed by an account. | ||
| * @param account The account that claimed the yield. | ||
| * @param amount The amount of yield claimed. | ||
| */ | ||
| event YieldClaimed(address indexed account, uint256 amount); | ||
|
|
||
| /// @custom:storage-location erc7201:noble.storage.USDN | ||
| struct USDNStorage { | ||
| uint128 index; | ||
| mapping(address account => uint256) principal; | ||
| uint256 totalPrincipal; | ||
| } | ||
|
|
||
| // keccak256(abi.encode(uint256(keccak256("noble.storage.USDN")) - 1)) & ~bytes32(uint256(0xff)) | ||
| bytes32 private constant USDNStorageLocation = 0xccec1a0a356b34ea3899fbc248aeaeba5687659563a3acddccc6f1e8a5d84200; | ||
|
|
||
| function _getUSDNStorage() private pure returns (USDNStorage storage $) { | ||
| assembly { | ||
| $.slot := USDNStorageLocation | ||
| } | ||
| } | ||
|
|
||
| constructor(address mailbox_) HypERC20(6, 1, mailbox_) {} | ||
|
|
||
| function initialize(address hook_, address ism_) public virtual initializer { | ||
| super.initialize("Noble Dollar", "USDN", hook_, ism_, msg.sender); | ||
|
|
||
| USDNStorage storage $ = _getUSDNStorage(); | ||
| $.index = 1e12; | ||
| } | ||
|
|
||
| /// @dev Returns the current index used for yield calculations. | ||
| function index() public view returns (uint256) { | ||
| return _getUSDNStorage().index; | ||
| } | ||
|
|
||
| /// @dev Returns the amount of principal in existence. | ||
| function totalPrincipal() public view returns (uint256) { | ||
| return _getUSDNStorage().totalPrincipal; | ||
| } | ||
|
|
||
| /// @dev Returns the amount of principal owned for a given account. | ||
| function principalOf(address account) public view returns (uint256) { | ||
| return _getUSDNStorage().principal[account]; | ||
| } | ||
|
|
||
| /** | ||
| * @notice Returns the amount of yield claimable for a given account. | ||
| * @dev Calculates claimable yield by comparing the expected balance (principal * current index) | ||
| * with the actual token balance. The yield represents the difference between what the | ||
| * account should have based on yield accrual and what they currently hold. | ||
| * | ||
| * Formula: max(0, (principal * index / 1e12) - currentBalance) | ||
| * | ||
| * Returns 0 if the current balance is greater than or equal to the expected balance, | ||
| * which can happen if the account has already claimed their yield or if no yield | ||
| * has accrued since their last interaction. | ||
| * | ||
| * @param account The address to check yield for. | ||
| * @return The amount of yield claimable by the account. | ||
| */ | ||
| function yield(address account) public view returns (uint256) { | ||
| USDNStorage storage $ = _getUSDNStorage(); | ||
| uint256 expectedBalance = $.principal[account] * $.index / 1e12; | ||
| uint256 currentBalance = balanceOf(account); | ||
|
|
||
| return expectedBalance > currentBalance ? expectedBalance - currentBalance : 0; | ||
| } | ||
|
|
||
| /** | ||
| * @notice Claims all available yield for the caller. | ||
| * @dev Calculates the claimable yield based on the difference between the expected balance | ||
| * (principal * current index) and the actual token balance. Transfers the yield amount | ||
| * from the contract to the caller and emits a YieldClaimed event. | ||
| * @custom:throws NoClaimableYield if the caller has no yield available to claim. | ||
| * @custom:emits YieldClaimed when yield is successfully claimed. | ||
| */ | ||
| function claim() public { | ||
| uint256 amount = yield(msg.sender); | ||
| if (amount == 0) { | ||
| revert NoClaimableYield(); | ||
| } | ||
|
|
||
| _transfer(address(this), msg.sender, amount); | ||
|
|
||
| emit YieldClaimed(msg.sender, amount); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Internal function that handles token transfers while managing principal accounting. | ||
| * @dev Overrides the base ERC20 _update function to implement yield-bearing token mechanics. | ||
| * This function manages principal balances and index updates for different transfer scenarios: | ||
| * | ||
| * 1. Yield payout (from contract): No principal updates needed | ||
| * 2. Yield accrual (to contract from zero address): Updates index based on new yield | ||
| * 3. Regular transfers: Updates principal balances for both sender and recipient | ||
| * 4. Minting (from zero address): Increases recipient's principal and total principal | ||
| * 5. Burning (to zero address): Decreases sender's principal and total principal | ||
| * | ||
| * @param from The address tokens are transferred from (zero address for minting) | ||
| * @param to The address tokens are transferred to (zero address for burning) | ||
| * @param value The amount of tokens being transferred | ||
| * | ||
| * @custom:throws InvalidTransfer if attempting to transfer to the contract from a non-zero address | ||
| * @custom:emits IndexUpdated when yield is accrued and the index is updated | ||
| * @custom:security Principal is calculated using ceiling division to prevent rounding errors | ||
| */ | ||
| function _update(address from, address to, uint256 value) internal virtual override { | ||
| USDNStorage storage $ = _getUSDNStorage(); | ||
|
|
||
| super._update(from, to, value); | ||
|
|
||
| if (from == address(this)) { | ||
| // We don't want to perform any principal updates in the case of yield payout. | ||
| return; | ||
| } | ||
| if (to == address(this)) { | ||
| if (from == address(0)) { | ||
| // We don't want to perform any principal updates in the case of yield accrual. | ||
| uint128 oldIndex = $.index; | ||
|
|
||
| $.index = uint128(totalSupply() * 1e12 / $.totalPrincipal); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| emit IndexUpdated(oldIndex, $.index, $.totalPrincipal, value); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| // We don't want to allow any other transfers to the yield account. | ||
| revert InvalidTransfer(); | ||
| } | ||
|
|
||
| uint256 principal = ((value * 1e12) + $.index - 1) / $.index; | ||
|
|
||
| // We don't want to update the sender's principal in the case of issuance. | ||
| if (from != address(0)) { | ||
| $.principal[from] -= principal; | ||
| } else { | ||
| $.totalPrincipal += principal; | ||
| } | ||
|
|
||
| // We don't want to update the recipient's principal in the case of withdrawal. | ||
| if (to != address(0)) { | ||
| if (from == address(0)) { | ||
| principal = (value * 1e12) / $.index; | ||
| } | ||
|
|
||
| $.principal[to] += principal; | ||
| } else { | ||
| $.totalPrincipal -= principal; | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.