diff --git a/README.md b/README.md index 4a0e4b6..57a67d0 100644 --- a/README.md +++ b/README.md @@ -2,22 +2,9 @@ This smart contract implements a custom rebasing ERC-20 token with additional features like pausing, block/unblock, access control, and upgradability. The contract aims to reflect the T-Bills APY into the token value through a reward multiplier mechanism. Users receive a proportional number of shares when they deposit tokens, and the number of tokens they can withdraw is calculated based on the current reward multiplier. The addRewardMultiplier function is called once a day to adjust the reward multiplier, ensuring accurate reflection of the yield from 3 months maturity T-Bills. -## Features - -- OpenZeppelin Access Control -- Rebasing token mechanism -- Minting and burning functionality -- Block/Unblock accounts -- Pausing emergency stop mechanism -- Reward multiplier system -- EIP-2612 permit support -- OpenZeppelin UUPS upgrade pattern - ## Dev -### Contributing - -This project uses Hardhat. It includes a contract, its tests, and a script that deploys the contract. +This Project uses Hardhat. It includes a contract, its tests, and a script that deploys the contract. > Prerequisites: Node v18 LTS @@ -74,8 +61,8 @@ npx hardhat compile Deploying and contract verification ```shell -npx hardhat run scripts/deploy.ts--network goerli -npx hardhat verify --network goerli +npx hardhat run scripts/deploy.ts --network goerli +npx hardhat verify --network goerli ``` Help @@ -84,6 +71,17 @@ Help npx hardhat help ``` +### Features + +- Access Control +- Rebasing token mechanism +- Minting and burning functionality +- Block/Unblock accounts +- Pausing emergency stop mechanism +- Reward multiplier system +- EIP-2612 permit support +- OpenZeppelin UUPS upgrade pattern + ### Functions #### Public and External Functions @@ -96,14 +94,14 @@ npx hardhat help - `convertToTokens(uint256 shares)`: Converts an amount of shares to tokens. - `totalShares()`: Returns the total amount of shares. - `totalSupply()`: Returns the total supply. -- `balanceOf(address account)`: Returns the balance of account. -- `sharesOf(address account)`: Returns the shares of account. +- `balanceOf(address account)`: Returns the account blanace. +- `sharesOf(address account)`: Returns the account shares. - `mint(address to, uint256 amount)`: Creates new tokens to the specified address. - `burn(address from, uint256 amount)`: Destroys tokens from the specified address. - `transfer(address to, uint256 amount)`: Transfers tokens between addresses. - `blockAccounts(address[] addresses)`: Blocks multiple accounts at once. - `unblockAccounts(address[] addresses)`: Unblocks multiple accounts at once. -- `isBlocked(address account)`: Checks if account is blocked. +- `isBlocked(address account)`: Checks if an account is blocked. - `pause()`: Pauses the contract, halting token transfers. - `unpause()`: Unpauses the contract, allowing token transfers. - `setRewardMultiplier(uint256 _rewardMultiplier)`: Sets the reward multiplier. @@ -126,8 +124,8 @@ npx hardhat help - `_afterTokenTransfer(address from, address to, uint256 amount)`: Hook that is called after any transfer of tokens. - `_transfer(address from, address to, uint256 amount)`: Internal function to transfer tokens between addresses. - `_blockAccount(address account)`: Internal function to block account. -- `_unblockAccount(address account)`: Internal function to unblock account. -- `_setRewardMultiplier(uint256 _rewardMultiplier)`: Internal function to sets the reward multiplier. +- `_unblockAccount(address account)`: Internal function to unblock an account. +- `_setRewardMultiplier(uint256 _rewardMultiplier)`: Internal function to set the reward multiplier. - `_spendAllowance(address owner, address spender, uint256 amount)`: Internal function to spend an allowance. - `_useNonce(address owner)`: Increments and returns the current nonce for a given address. - `_approve(address owner, address spender, uint256 amount)`: Internal function to approve an allowance for a spender. @@ -150,3 +148,25 @@ npx hardhat help - `ORACLE_ROLE`: Grants the ability to update the reward multiplier. - `UPGRADE_ROLE`: Grants the ability to upgrade the contract. - `PAUSE_ROLE`: Grants the ability to pause/unpause the contract. + +## Contributing + +Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. + +Please make sure to run the linter and update tests as appropriate. + +## Security + +The security policy is detailed in [SECURITY.md](https://github.com/mountainprotocol/tokens/blob/main/SECURITY.md), and specifies how you can report security vulnerabilities and which versions will receive security updates. We run a bug bounty program on Immunefi to reward the responsible disclosure of vulnerabilities. + +## Audits + +Audits can be found at https://github.com/mountainprotocol/audits. + +## License + +Mountain Protocol Contracts are released under the [MIT License](https://github.com/mountainprotocol/tokens/blob/main/LICENSE). + +## Legal + +The use of this Project is governed by the terms found at https://docs.mountainprotocol.com/legal/terms-and-conditions. \ No newline at end of file diff --git a/contracts/USDM.sol b/contracts/USDM.sol index 8acb8bb..d7d0463 100644 --- a/contracts/USDM.sol +++ b/contracts/USDM.sol @@ -64,7 +64,7 @@ contract USDM is * Standard ERC20 Errors * @dev See https://eips.ethereum.org/EIPS/eip-6093 */ - error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); + error ERC20InsufficientBalance(address sender, uint256 shares, uint256 sharesNeeded); error ERC20InvalidSender(address sender); error ERC20InvalidReceiver(address receiver); error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); @@ -76,7 +76,7 @@ contract USDM is // USDM Errors error USDMInvalidMintReceiver(address receiver); error USDMInvalidBurnSender(address sender); - error USDMInsufficientBurnBalance(address sender, uint256 balance, uint256 needed); + error USDMInsufficientBurnBalance(address sender, uint256 shares, uint256 sharesNeeded); error USDMInvalidRewardMultiplier(uint256 rewardMultiplier); error USDMBlockedSender(address sender); error USDMInvalidBlockedAccount(address account); @@ -500,7 +500,7 @@ contract USDM is * @param _rewardMultiplierIncrement The amount to add to the current reward multiplier */ function addRewardMultiplier(uint256 _rewardMultiplierIncrement) external onlyRole(ORACLE_ROLE) { - if (_rewardMultiplierIncrement <= 0) { + if (_rewardMultiplierIncrement == 0) { revert USDMInvalidRewardMultiplier(_rewardMultiplierIncrement); } diff --git a/test/USDMTest.t.sol b/test/USDMTest.t.sol deleted file mode 100644 index 9f6422a..0000000 --- a/test/USDMTest.t.sol +++ /dev/null @@ -1,138 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.18; - -import {Test} from "forge-std/Test.sol"; -// import {console} from "forge-std/console.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; - -import "../contracts/USDM.sol"; - -contract UUPSProxy is ERC1967Proxy { - constructor(address _implementation, bytes memory _data) ERC1967Proxy(_implementation, _data) {} -} - -contract Handler { - USDM public usdm; - - constructor(USDM _usdm) { - usdm = _usdm; - } -} - -contract USDMInvariants is Test { - USDM implementation; - UUPSProxy proxy; - USDM usdm; - Handler handler; - - function setUp() public { - // deploy implementation contract - implementation = new USDM(); - - // deploy proxy contract and point it to implementation - proxy = new UUPSProxy(address(implementation), ""); - - // wrap in ABI to support easier calls - usdm = USDM(address(proxy)); - - usdm.initialize("Mountain Protocol USD", "USDM", 0); - - // Grant minter role - usdm.grantRole(keccak256("MINTER_ROLE"), address(this)); - // Grant burner role - usdm.grantRole(keccak256("BURNER_ROLE"), address(this)); - // Grant oracle role - usdm.grantRole(keccak256("ORACLE_ROLE"), address(this)); - - handler = new Handler(usdm); - - // targetContract(address(handler)); - // targetContract(address(implementation)); - } - - function test_minting(uint256 amountToMint) external { - amountToMint = bound(amountToMint, 1e18, 1e28); - - uint256 sharesBefore = usdm.sharesOf(address(this)); - - usdm.mint(address(this), amountToMint); - - uint256 sharesAfter = usdm.sharesOf(address(this)); - - assertLt(sharesBefore, sharesAfter); - } - - function test_burning(uint256 amountToBurn) external { - amountToBurn = bound(amountToBurn, 1e18, 1e28); - - usdm.mint(address(this), amountToBurn); - - vm.expectRevert(); - - usdm.burn(address(this), amountToBurn + 1); - - uint256 sharesBefore = usdm.sharesOf(address(this)); - - usdm.burn(address(this), amountToBurn); - - uint256 sharesAfter = usdm.sharesOf(address(this)); - - assertGt(sharesBefore, sharesAfter); - } - - function test_transfering(uint256 amount, address to) external { - amount = bound(amount, 0, 1e28); - vm.assume(to != address(0)); - - usdm.mint(address(this), amount); - - uint256 sharesBeforeFrom = usdm.sharesOf(address(this)); - uint256 sharesBeforeTo = usdm.sharesOf(to); - - usdm.transfer(to, amount); - - uint256 sharesAfterFrom = usdm.sharesOf(address(this)); - uint256 sharesAfterTo = usdm.sharesOf(to); - - assertGe(sharesBeforeFrom, sharesAfterFrom); - assertLe(sharesBeforeTo, sharesAfterTo); - } - - // totalSupply should be ge than totalShares [invariant] - function invariant_totalSupplyGeTotalShares() public { - assertGe(usdm.totalSupply(), usdm.totalShares()); - } - - // totalSupply should be gt than totalShares when rewardMultiplier is > 1 [invariant] - function test_totalSupplyGtTotalSharesWhenRewardMultiplier(uint256 yield, address to, uint256 amount) public { - yield = bound(yield, 3e11, 66e14); // 0.00003% - 0.66% - amount = bound(amount, 1e21, 1e28); // 1k - 10B - vm.assume(to != address(0)); - - usdm.addRewardMultiplier(yield); - usdm.mint(to, amount); - - assertGt(usdm.totalSupply(), usdm.totalShares()); - } - - // transfer shouldn't change totalSupply - function test_transferDoesntChangeTotalSupply(uint256 amount, address to) external { - vm.assume(amount < 1e28); - vm.assume(to != address(0)); - - usdm.mint(address(this), amount); - - uint256 totalSupplyBefore = usdm.totalSupply(); - - usdm.transfer(to, amount); - - uint256 totalSupplyAfter = usdm.totalSupply(); - - assertEq(totalSupplyBefore, totalSupplyAfter); - } - - // Sum of all shares should be equal to totalShares [invariant] - // balance of address zero should be always zero [invariant] - // only mint and burn should change totalShares [invariant] !!! - // an address can only transfer at max its own balance and its allowances [invariant] !!! -}