diff --git a/.prettierrc b/.prettierrc index a152663..7860c31 100644 --- a/.prettierrc +++ b/.prettierrc @@ -5,7 +5,7 @@ "files": "*.sol", "options": { "bracketSpacing": true, - "compiler": "0.8.26", + "compiler": "0.8.23", "parser": "solidity-parse", "printWidth": 120, "tabWidth": 4 diff --git a/.solhint.json b/.solhint.json index 38a65d9..72cd17f 100644 --- a/.solhint.json +++ b/.solhint.json @@ -4,7 +4,7 @@ "rules": { "prettier/prettier": "error", "code-complexity": ["warn", 10], - "compiler-version": ["error", "0.8.26"], + "compiler-version": ["error", "0.8.23"], "comprehensive-interface": "off", "const-name-snakecase": "off", "func-name-mixedcase": "off", diff --git a/foundry.toml b/foundry.toml index 94a5cef..f853970 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,7 @@ gas_reports = ["InterestBearingToken"] gas_reports_ignore = [] ignored_error_codes = [] optimizer = false -solc_version = "0.8.26" +solc_version = "0.8.23" verbosity = 3 [profile.production] diff --git a/package-lock.json b/package-lock.json index a12cdae..e1e746c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "solmate": "6.2.0" }, "devDependencies": { + "common": "github:@mzero-labs/common#main", "forge-std": "github:foundry-rs/forge-std#v1.8.1", "husky": "^9.0.11", "lint-staged": "^15.2.2", @@ -520,6 +521,15 @@ "node": ">=14" } }, + "node_modules/common": { + "name": "@mzero-labs/common", + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/mzero-labs/common.git#e809402c4cc21f1fa8291f17ee0aee859f3b0d29", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", diff --git a/package.json b/package.json index 46cc3ae..6101900 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ }, "devDependencies": { "forge-std": "github:foundry-rs/forge-std#v1.8.1", + "common": "github:@mzero-labs/common#main", "husky": "^9.0.11", "lint-staged": "^15.2.2", "prettier": "^3.2.5", diff --git a/remappings.txt b/remappings.txt index ee60c65..ae5c426 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,2 +1,3 @@ forge-std/=node_modules/forge-std/src -solmate/=node_modules/solmate/src \ No newline at end of file +solmate/=node_modules/solmate/src +@mzero-labs/=node_modules/common/src \ No newline at end of file diff --git a/script/InterestBearingToken.s.sol b/script/InterestBearingToken.s.sol index b22d815..d860631 100644 --- a/script/InterestBearingToken.s.sol +++ b/script/InterestBearingToken.s.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.26; +pragma solidity 0.8.23; import { Script } from "forge-std/Script.sol"; import { InterestBearingToken } from "../src/InterestBearingToken.sol"; diff --git a/src/InterestBearingToken.sol b/src/InterestBearingToken.sol index ced1b6f..e0f6f13 100644 --- a/src/InterestBearingToken.sol +++ b/src/InterestBearingToken.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.26; +pragma solidity 0.8.23; -import { ERC20 } from "solmate/tokens/ERC20.sol"; import { Owned } from "solmate/auth/Owned.sol"; +import { ERC20Extended } from "@mzero-labs/ERC20Extended.sol"; // import { console } from "forge-std/console.sol"; -contract InterestBearingToken is ERC20, Owned { +contract InterestBearingToken is ERC20Extended, Owned { /* ============ Events ============ */ /** @@ -14,16 +14,13 @@ contract InterestBearingToken is ERC20, Owned { * @param account The account that started earning. */ event StartedEarning(address indexed account); - event YearlyRateUpdated(uint16 oldRate, uint16 newRate); /* ============ Structs ============ */ // nothing for now /* ============ Errors ============ */ - error InvalidRecipient(address recipient); error InvalidYearlyRate(uint16 rate); - error InsufficientAmount(uint256 amount); error InsufficientBalance(uint256 amount); /* ============ Variables ============ */ @@ -32,16 +29,18 @@ contract InterestBearingToken is ERC20, Owned { uint16 public constant MIN_YEARLY_RATE = 100; // 1% APY in BPS uint16 public constant MAX_YEARLY_RATE = 4000; // 40% APY as max + uint256 internal _totalSupply; uint16 public yearlyRate; // interest rate in BPS beetween 100 (1%) and 40000 (40%) - mapping(address => uint256) internal lastUpdateTimestamp; - mapping(address => uint256) internal accruedInterest; + mapping(address => uint256) internal _balances; + mapping(address => uint256) internal _lastUpdateTimestamp; + mapping(address => uint256) internal _accruedInterest; /* ============ Modifiers ============ */ // nothing for now /* ============ Constructor ============ */ - constructor(uint16 yearlyRate_) ERC20("IBToken", "IB", 6) Owned(msg.sender) { + constructor(uint16 yearlyRate_) ERC20Extended("IBToken", "IB", 6) Owned(msg.sender) { setYearlyRate(yearlyRate_); } @@ -64,6 +63,7 @@ contract InterestBearingToken is ERC20, Owned { function burn(uint256 amount_) external { _revertIfInsufficientAmount(amount_); + //claimRewards ? _revertIfInsufficientBalance(msg.sender, amount_); address caller = msg.sender; _updateRewards(caller); @@ -74,42 +74,78 @@ contract InterestBearingToken is ERC20, Owned { _updateRewards(account_); } - function transfer(address to, uint256 amount) public override returns (bool) { - _updateRewards(msg.sender); - _updateRewards(to); - return super.transfer(to, amount); + function _transfer(address sender_, address recipient_, uint256 amount_) internal override { + _revertIfInvalidRecipient(recipient_); + + _updateRewards(sender_); + _updateRewards(recipient_); + + _balances[sender_] -= amount_; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + _balances[recipient_] += amount_; + } + + emit Transfer(sender_, recipient_, amount_); } - function transferFrom(address from, address to, uint256 amount) public override returns (bool) { - _updateRewards(from); - _updateRewards(to); - return super.transferFrom(from, to, amount); + function balanceOf(address account_) external view override returns (uint256) { + return _balances[account_] + _accruedInterest[account_]; } - function totalBalance(address account_) external view returns (uint256) { - return this.balanceOf(account_) + accruedInterest[account_]; + function totalSupply() external view returns (uint256 totalSupply_) { + unchecked { + // return totalNonEarningSupply + totalEarningSupply(); + return _totalSupply; + } } /* ============ Internal Interactive Functions ============ */ + function _mint(address to, uint256 amount) internal virtual { + _totalSupply += amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + _balances[to] += amount; + } + + emit Transfer(address(0), to, amount); + } + + function _burn(address from, uint256 amount) internal virtual { + _balances[from] -= amount; + + // Cannot underflow because a user's balance + // will never be larger than the total supply. + unchecked { + _totalSupply -= amount; + } + + emit Transfer(from, address(0), amount); + } + function _updateRewards(address account_) internal { uint256 timestamp = block.timestamp; - if (lastUpdateTimestamp[account_] == 0) { - lastUpdateTimestamp[account_] = timestamp; + if (_lastUpdateTimestamp[account_] == 0) { + _lastUpdateTimestamp[account_] = timestamp; emit StartedEarning(account_); return; } // the interest calculation is using the raw balance - uint256 rawBalance = this.balanceOf(account_); + uint256 rawBalance = _balances[account_]; - // Safe to use unchecked here, since `block.timestamp` is always greater than `lastUpdateTimestamp[account_]`. + // Safe to use unchecked here, since `block.timestamp` is always greater than `_lastUpdateTimestamp[account_]`. unchecked { - uint256 timeElapsed = timestamp - lastUpdateTimestamp[account_]; + uint256 timeElapsed = timestamp - _lastUpdateTimestamp[account_]; uint256 interest = (rawBalance * timeElapsed * yearlyRate) / (10_000 * uint256(SECONDS_PER_YEAR)); - accruedInterest[account_] += interest; + _accruedInterest[account_] += interest; } - lastUpdateTimestamp[account_] = block.timestamp; + _lastUpdateTimestamp[account_] = block.timestamp; } /** @@ -118,7 +154,7 @@ contract InterestBearingToken is ERC20, Owned { * @param amount_ Balance to check. */ function _revertIfInsufficientBalance(address caller_, uint256 amount_) internal view { - uint256 balance = this.balanceOf(caller_); + uint256 balance = _balances[caller_]; if (balance < amount_) revert InsufficientBalance(amount_); } diff --git a/test/InterestBearingToken.t.sol b/test/InterestBearingToken.t.sol index 8c8041b..0893e99 100644 --- a/test/InterestBearingToken.t.sol +++ b/test/InterestBearingToken.t.sol @@ -1,12 +1,9 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.26; +pragma solidity 0.8.23; import { Test, console } from "forge-std/Test.sol"; import { InterestBearingToken } from "../src/InterestBearingToken.sol"; - -interface IERC20 { - function balanceOf(address account) external view returns (uint256); -} +import { IERC20Extended } from "@mzero-labs/interfaces/IERC20Extended.sol"; contract InterestBearingTokenTest is Test { InterestBearingToken token; @@ -21,8 +18,6 @@ contract InterestBearingTokenTest is Test { uint256 constant INSUFFICIENT_AMOUNT = 0; uint16 constant INTEREST_RATE = 1000; // 10% APY in BPS - error InvalidRecipient(address recipient); - error InsufficientAmount(uint256 amount); error InsufficientBalance(uint256 amount); event StartedEarning(address indexed account); @@ -67,13 +62,13 @@ contract InterestBearingTokenTest is Test { function testMintingInvalidRecipient() external { vm.prank(owner); - vm.expectRevert(abi.encodeWithSelector(InvalidRecipient.selector, address(0))); + vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InvalidRecipient.selector, address(0))); token.mint(address(0), INITIAL_SUPPLY); } function testMintingInsufficientAmount() external { vm.prank(owner); - vm.expectRevert(abi.encodeWithSelector(InsufficientAmount.selector, INSUFFICIENT_AMOUNT)); + vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, INSUFFICIENT_AMOUNT)); token.mint(alice, INSUFFICIENT_AMOUNT); } @@ -96,7 +91,7 @@ contract InterestBearingTokenTest is Test { function testBurningFailsWithInsufficientAmount() public { _mint(owner, alice, INITIAL_SUPPLY); vm.prank(alice); - vm.expectRevert(abi.encodeWithSelector(InsufficientAmount.selector, INSUFFICIENT_AMOUNT)); + vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InsufficientAmount.selector, INSUFFICIENT_AMOUNT)); token.burn(INSUFFICIENT_AMOUNT); } @@ -126,20 +121,18 @@ contract InterestBearingTokenTest is Test { uint interest = (INITIAL_SUPPLY * INTEREST_RATE * 365 days) / (10000 * 365 days); uint256 expectedFinalBalance = INITIAL_SUPPLY + interest; - assertEq(token.totalBalance(alice), expectedFinalBalance); + assertEq(token.balanceOf(alice), expectedFinalBalance); } function testInterestAccrualWithMultipleMints() external { _mint(owner, alice, INITIAL_SUPPLY); uint256 balanceRaw = INITIAL_SUPPLY; - assertEq(token.balanceOf(alice), balanceRaw); vm.warp(block.timestamp + 180 days); // Calculate interest for the first 180 days uint256 firstPeriodInterest = (balanceRaw * INTEREST_RATE * 180 days) / (10000 * 365 days); _mint(owner, alice, INITIAL_SUPPLY); balanceRaw += INITIAL_SUPPLY; - assertEq(token.balanceOf(alice), balanceRaw); vm.warp(block.timestamp + 185 days); // total 365 days from first mint @@ -149,7 +142,7 @@ contract InterestBearingTokenTest is Test { // The expected final balance includes the initial supplies and accrued interests token.updateInterest(alice); uint256 expectedFinalBalance = balanceRaw + firstPeriodInterest + secondPeriodInterest; - assertEq(token.totalBalance(alice), expectedFinalBalance); + assertEq(token.balanceOf(alice), expectedFinalBalance); } function testInterestAccrualWithRateChange() external { @@ -162,13 +155,11 @@ contract InterestBearingTokenTest is Test { // First mint and time warp _mint(owner, alice, INITIAL_SUPPLY); uint256 balanceRaw = INITIAL_SUPPLY; - assertEq(token.balanceOf(alice), balanceRaw); vm.warp(block.timestamp + 180 days); // Calculate interest for the first 180 days with the initial rate token.updateInterest(alice); uint256 firstPeriodInterest = (balanceRaw * initialRate * 180 days) / (10_000 * 365 days); - assertEq(token.totalBalance(alice), balanceRaw + firstPeriodInterest); // Change the interest rate midway vm.prank(owner); @@ -178,7 +169,6 @@ contract InterestBearingTokenTest is Test { uint256 tokensToMint = 500 * 10e6; _mint(owner, alice, tokensToMint); balanceRaw += tokensToMint; - assertEq(token.balanceOf(alice), balanceRaw); vm.warp(block.timestamp + 30 days); @@ -188,7 +178,7 @@ contract InterestBearingTokenTest is Test { // // Update interests and verify the final balance token.updateInterest(alice); uint256 expectedFinalBalance = balanceRaw + firstPeriodInterest + secondPeriodInterest; - assertEq(token.totalBalance(alice), expectedFinalBalance); + assertEq(token.balanceOf(alice), expectedFinalBalance); } function testInterestAccrualWithoutBalanceChange() external { @@ -200,7 +190,7 @@ contract InterestBearingTokenTest is Test { // Calculate interest for the first 10 days uint256 firstPeriodInterest = (balanceRaw * INTEREST_RATE * 10 days) / (10000 * 365 days); token.updateInterest(alice); - assertEq(token.totalBalance(alice), balanceRaw + firstPeriodInterest); + assertEq(token.balanceOf(alice), balanceRaw + firstPeriodInterest); // Warp time forward without changing the balance vm.warp(block.timestamp + 18 days); @@ -209,7 +199,7 @@ contract InterestBearingTokenTest is Test { uint256 secondPeriodInterest = (balanceRaw * INTEREST_RATE * 18 days) / (10000 * 365 days); token.updateInterest(alice); uint256 expectedFinalBalance = balanceRaw + firstPeriodInterest + secondPeriodInterest; - assertEq(token.totalBalance(alice), expectedFinalBalance); + assertEq(token.balanceOf(alice), expectedFinalBalance); } function testSetYearlyRate() public { @@ -255,14 +245,11 @@ contract InterestBearingTokenTest is Test { // Calculate interest for the first 180 days uint256 firstPeriodInterestAlice = (aliceBalanceRaw * INTEREST_RATE * 180 days) / (10000 * 365 days); token.updateInterest(alice); - assertEq(token.totalBalance(alice), aliceBalanceRaw + firstPeriodInterestAlice); // Transfer tokens from Alice to Bob _transfer(alice, bob, TRANSFER_AMOUNT); aliceBalanceRaw -= TRANSFER_AMOUNT; uint256 bobBalanceRaw = TRANSFER_AMOUNT; - assertEq(token.balanceOf(alice), aliceBalanceRaw); - assertEq(token.balanceOf(bob), TRANSFER_AMOUNT); // Calculate interest for the next 185 days with updated balance vm.warp(block.timestamp + 185 days); @@ -277,8 +264,8 @@ contract InterestBearingTokenTest is Test { uint256 expectedFinalBalanceAlice = aliceBalanceRaw + firstPeriodInterestAlice + secondPeriodInterestAlice; uint256 expectedFinalBalanceBob = bobBalanceRaw + secondPeriodInterestBob; - assertEq(token.totalBalance(alice), expectedFinalBalanceAlice); - assertEq(token.totalBalance(bob), expectedFinalBalanceBob); + assertEq(token.balanceOf(alice), expectedFinalBalanceAlice); + assertEq(token.balanceOf(bob), expectedFinalBalanceBob); } /* ============ Helper functions ============ */