Skip to content

Commit

Permalink
chore: replace solmate ERC20 with mzerolabs ERC20Extended
Browse files Browse the repository at this point in the history
  • Loading branch information
jpgonzalezra committed May 26, 2024
1 parent 88ef94b commit 234badd
Show file tree
Hide file tree
Showing 9 changed files with 92 additions and 57 deletions.
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"files": "*.sol",
"options": {
"bracketSpacing": true,
"compiler": "0.8.26",
"compiler": "0.8.23",
"parser": "solidity-parse",
"printWidth": 120,
"tabWidth": 4
Expand Down
2 changes: 1 addition & 1 deletion .solhint.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
forge-std/=node_modules/forge-std/src
solmate/=node_modules/solmate/src
solmate/=node_modules/solmate/src
@mzero-labs/=node_modules/common/src
2 changes: 1 addition & 1 deletion script/InterestBearingToken.s.sol
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
90 changes: 63 additions & 27 deletions src/InterestBearingToken.sol
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
// 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 ============ */

/**
* @notice Emmited when the account starts earning token
* @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 ============ */
Expand All @@ -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_);
}

Expand All @@ -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);
Expand All @@ -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;
}

/**
Expand All @@ -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_);
}

Expand Down
37 changes: 12 additions & 25 deletions test/InterestBearingToken.t.sol
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}

Expand All @@ -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);
}

Expand Down Expand Up @@ -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

Expand All @@ -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 {
Expand All @@ -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);
Expand All @@ -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);

Expand All @@ -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 {
Expand All @@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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 ============ */
Expand Down

0 comments on commit 234badd

Please sign in to comment.