diff --git a/README.md b/README.md index da3f79b..bba5214 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Overview -The USTB Token project is a Solidity-based smart contract system designed to provide a rebase token with cross-chain and Layer Zero functionalities. It's built with upgradeability in mind, ensuring that the token logic can evolve over time. +The USTB Token project is a Solidity-based smart contract system designed to provide a rebase token with cross-chain and Layer Zero functionalities. It's built with upgradeability in mind, ensuring that the token logic can evolve. ## Key Components diff --git a/foundry.toml b/foundry.toml index fc4d67e..58815c2 100644 --- a/foundry.toml +++ b/foundry.toml @@ -37,4 +37,6 @@ unreal = { key = "", url = "https://unreal.blockscout.com/api" } [fmt] wrap_comments = true +[invariant] + # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/src/USTB.sol b/src/USTB.sol index f10545e..eedfe84 100644 --- a/src/USTB.sol +++ b/src/USTB.sol @@ -34,7 +34,8 @@ contract USTB is IUSTB, LayerZeroRebaseTokenUpgradeable, UUPSUpgradeable { } // keccak256(abi.encode(uint256(keccak256("tangible.storage.USTB")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 private constant USTBStorageLocation = 0x56cb630b12f1f031f72de1d734e98085323517cc6515c1c85452dc02f218dd00; + bytes32 private constant USTBStorageLocation = + 0x56cb630b12f1f031f72de1d734e98085323517cc6515c1c85452dc02f218dd00; function _getUSTBStorage() private pure returns (USTBStorage storage $) { // slither-disable-next-line assembly @@ -70,15 +71,18 @@ contract USTB is IUSTB, LayerZeroRebaseTokenUpgradeable, UUPSUpgradeable { * @param endpoint The Layer Zero endpoint for cross-chain operations. * @custom:oz-upgrades-unsafe-allow constructor */ - constructor(address underlying, uint256 mainChainId, address endpoint) - CrossChainToken(mainChainId) - LayerZeroRebaseTokenUpgradeable(endpoint) - { + constructor( + address underlying, + uint256 mainChainId, + address endpoint + ) CrossChainToken(mainChainId) LayerZeroRebaseTokenUpgradeable(endpoint) { UNDERLYING = underlying; _disableInitializers(); } - function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} + function _authorizeUpgrade( + address newImplementation + ) internal override onlyOwner {} /** * @notice Initializes the USTB contract with essential parameters. @@ -146,11 +150,20 @@ contract USTB is IUSTB, LayerZeroRebaseTokenUpgradeable, UUPSUpgradeable { _disableRebase(account, disable); } - function rebaseIndexManager() external view override returns (address _rebaseIndexManager) { + function rebaseIndexManager() + external + view + override + returns (address _rebaseIndexManager) + { USTBStorage storage $ = _getUSTBStorage(); _rebaseIndexManager = $.rebaseIndexManager; } + function isNotRebase(address account) external view returns (bool) { + return _isRebaseDisabled(account); + } + /** * @notice Sets the rebase index and its corresponding nonce on non-main chains. * @dev This function allows the rebase index manager to manually update the rebase index and nonce when not on the @@ -162,7 +175,10 @@ contract USTB is IUSTB, LayerZeroRebaseTokenUpgradeable, UUPSUpgradeable { * @param index The new rebase index to set. * @param nonce The new nonce corresponding to the rebase index. */ - function setRebaseIndex(uint256 index, uint256 nonce) public onlyIndexManager mainChain(false) { + function setRebaseIndex( + uint256 index, + uint256 nonce + ) public onlyIndexManager mainChain(false) { _setRebaseIndex(index, nonce); } @@ -210,7 +226,11 @@ contract USTB is IUSTB, LayerZeroRebaseTokenUpgradeable, UUPSUpgradeable { * @param to The address to which tokens are being transferred or minted. * @param amount The amount of tokens being transferred, minted, or burned. */ - function _update(address from, address to, uint256 amount) internal virtual override { + function _update( + address from, + address to, + uint256 amount + ) internal virtual override { refreshRebaseIndex(); super._update(from, to, amount); } diff --git a/test/Invariant/Handler.sol b/test/Invariant/Handler.sol new file mode 100644 index 0000000..bc19f49 --- /dev/null +++ b/test/Invariant/Handler.sol @@ -0,0 +1,329 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.20; + +import {USTB, IERC20} from "../../src/USTB.sol"; +import {CommonBase} from "forge-std/Base.sol"; +import {StdUtils} from "forge-std/StdUtils.sol"; +import {StdCheats} from "forge-std/StdCheats.sol"; +import {console2 as console} from "forge-std/Test.sol"; +import {AddressSet, LibAddressSet} from "./LibAddressSet.sol"; +import {RebaseTokenMath} from "tangible-foundation-contracts/libraries/RebaseTokenMath.sol"; + +contract Handler is CommonBase, StdCheats, StdUtils { + using RebaseTokenMath for uint256; + using LibAddressSet for AddressSet; + + AddressSet internal _actors; + + USTB public ustb; + USTB public ustb2; + IERC20 usdm; + + mapping(bytes32 => uint256) public calls; + + address currentActor; + uint256 public ghost_burntSum; + uint256 public ghost_zeroBurn; + + uint256 public ghost_zeroMint; + uint256 public ghost_mintedSum; + + uint256 public ghost_actualBurn; + uint256 public ghost_actualMint; + + uint256 public ghost_enableRebase; + uint256 public ghost_zeroTransfer; + + uint256 public ghost_disableRebase; + uint256 public ghost_actualSendFrom; + + uint256 public ghost_actualTransfer; + uint256 public ghost_bridgedTokensTo; + + uint256 public ghost_zeroAddressBurn; + uint256 public ghost_zeroTransferFrom; + + uint256 public ghost_bridgedTokensFrom; + uint256 public ghost_actualTransferFrom; + + uint256 public ghost_zeroAddressTransfer; + uint256 public ghost_zeroAddressSendFrom; + + uint256 public ghost_zeroAddressTransferFrom; + uint256 public ghost_zeroAddressDisableRebase; + + constructor(USTB _ustb, USTB _ustb2, address _usdm) { + ustb = _ustb; + ustb2 = _ustb2; + usdm = IERC20(_usdm); + } + + modifier createActor() { + currentActor = msg.sender; + _actors.add(currentActor); + _; + } + + modifier useActor(uint256 actorIndexSeed) { + currentActor = _actors.rand(actorIndexSeed); + _; + } + + modifier countCall(bytes32 key) { + calls[key]++; + _; + } + + function mint(uint256 amount) public createActor countCall("mint") { + amount = bound(amount, 0, type(uint96).max); + ghost_mintedSum += amount; + + if (amount == 0) ghost_zeroMint++; + if (amount > 0) ghost_actualMint++; + + address to = currentActor; + + __mint(currentActor, amount); + vm.startPrank(currentActor); + + usdm.approve(address(ustb), amount); + ustb.mint(to, amount); + } + + function burn( + uint seed, + uint256 amount + ) public useActor(seed) countCall("burn") { + if (currentActor != address(0)) { + amount = bound(amount, 0, ustb.balanceOf(currentActor)); + ghost_burntSum += amount; + + ghost_actualBurn++; + if (amount == 0) ghost_zeroBurn++; + + address from = currentActor; + + vm.startPrank(currentActor); + ustb.burn(from, amount); + } else ghost_zeroAddressBurn++; + } + + function approve( + uint256 actorSeed, + uint256 spenderSeed, + uint256 amount + ) public useActor(actorSeed) countCall("approve") { + address spender = _actors.rand(spenderSeed); + if (currentActor != address(0)) { + vm.startPrank(currentActor); + ustb.approve(spender, amount); + } + } + + function disable( + uint256 seed, + bool flag + ) public useActor(seed) countCall("disableRebase") { + if ( + currentActor != address(0) && flag != ustb.isNotRebase(currentActor) + ) { + vm.startPrank(currentActor); + + ustb.disableRebase(currentActor, flag); + } else ghost_zeroAddressDisableRebase++; + } + + function transfer( + uint256 actorSeed, + uint256 toSeed, + uint256 amount + ) public useActor(actorSeed) countCall("transfer") { + address to = _actors.rand(toSeed); + + if (currentActor != address(0)) { + vm.deal(currentActor, 1 ether); + amount = bound(amount, 0, ustb.balanceOf(currentActor)); + + ghost_actualTransfer++; + if (amount == 0) ghost_zeroTransfer++; + + vm.startPrank(currentActor); + ustb.transfer(to, amount); + } else ghost_zeroAddressTransfer++; + } + + function sendFrom( + uint256 actorSeed, + uint256 toSeed, + uint256 amount + ) public useActor(actorSeed) countCall("sendFrom") { + if (!ustb.isNotRebase(currentActor)) { + address to = _actors.rand(toSeed); + if (currentActor != address(0)) { + ghost_actualSendFrom++; + vm.deal(to, 10 ether); + + vm.deal(currentActor, 10 ether); + amount = bound(amount, 0, ustb.balanceOf(currentActor)); + + if (amount == 0) ghost_zeroTransfer++; + vm.startPrank(currentActor); + + usdm.approve(address(ustb), amount); + uint256 nativeFee; + + (nativeFee, ) = ustb.estimateSendFee( + uint16(block.chainid), + abi.encodePacked(to), + amount, + false, + "" + ); + + uint256 contractBalBeforeBridge = ustb.balanceOf(address(this)); + + ustb.sendFrom{value: (nativeFee * 105) / 100}( + currentActor, + uint16(block.chainid), + abi.encodePacked(to), + amount, + payable(currentActor), + address(0), + "" + ); + + uint256 contractBalAfterBridge = ustb.balanceOf(address(this)); + + ghost_bridgedTokensTo += + contractBalAfterBridge - + contractBalBeforeBridge; + + vm.startPrank(to); + + (nativeFee, ) = ustb.estimateSendFee( + uint16(block.chainid), + abi.encodePacked(currentActor), + ustb2.balanceOf(to), + false, + "" + ); + + uint256 contractBalBeforeBridge0 = ustb.balanceOf( + address(this) + ); + + ustb2.sendFrom{value: (nativeFee * 105) / 100}( + to, + uint16(block.chainid), + abi.encodePacked(currentActor), + ustb2.balanceOf(to), + payable(to), + address(0), + "" + ); + + uint256 contractBalAfterBridge0 = ustb2.balanceOf( + address(this) + ); + + ghost_bridgedTokensFrom += + contractBalBeforeBridge0 - + contractBalAfterBridge0; + } else ghost_zeroAddressSendFrom++; + } + } + + function transferFrom( + uint256 actorSeed, + uint256 fromSeed, + uint256 toSeed, + bool _approve, + uint256 amount + ) public useActor(actorSeed) countCall("transferFrom") { + address from = _actors.rand(fromSeed); + address to = _actors.rand(toSeed); + amount = bound(amount, 0, ustb.balanceOf(from)); + + if (currentActor != address(0)) { + if (_approve) { + vm.startPrank(from); + ustb.approve(currentActor, amount); + vm.stopPrank(); + } else { + amount = bound(amount, 0, ustb.allowance(from, currentActor)); + } + + ghost_actualTransferFrom++; + if (amount == 0) ghost_zeroTransferFrom++; + + vm.startPrank(currentActor); + ustb.transferFrom(from, to, amount); + + vm.stopPrank(); + } else ghost_zeroAddressTransferFrom++; + } + + function reduceActors( + uint256 acc, + function(uint256, address) external returns (uint256) func + ) public returns (uint256) { + return _actors.reduce(acc, func); + } + + function forEachActor(function(address) external func) public { + return _actors.forEach(func); + } + + function callSummary() external view { + console.log("-------------------"); + console.log(" "); + console.log("Call summary:"); + console.log(" "); + + console.log("-------------------"); + console.log("Call Count:"); + console.log("-------------------"); + console.log("Mint(s)", calls["mint"]); + console.log("Burn(s)", calls["burn"]); + console.log("Approve(s)", calls["approve"]); + console.log("Transfer(s):", calls["transfer"]); + console.log("SendFrom(s):", calls["sendFrom"]); + console.log("TransferFrom(s):", calls["transferFrom"]); + console.log("DisableRebase(s):", calls["disableRebase"]); + + console.log("-------------------"); + console.log("Zero Calls:"); + console.log("-------------------"); + console.log("Mint(s):", ghost_zeroMint); + console.log("Burn(s):", ghost_zeroBurn); + console.log("Transfer(s):", ghost_zeroTransfer); + console.log("TransferFrom(s):", ghost_zeroTransferFrom); + + console.log("-------------------"); + console.log("Zero Address Call:"); + console.log("-------------------"); + console.log("Burn(s):", ghost_zeroAddressBurn); + console.log("Transfer(s):", ghost_zeroAddressTransfer); + console.log("sendFrom(s):", ghost_zeroAddressSendFrom); + console.log("TransferFrom(s):", ghost_zeroAddressTransferFrom); + console.log("DisableRebase(s):", ghost_zeroAddressDisableRebase); + + console.log("-------------------"); + console.log("Actual Calls:"); + console.log("-------------------"); + console.log("Mint(s):", ghost_actualMint); + console.log("Burn(s):", ghost_actualBurn); + console.log("SendFrom(s):", ghost_actualSendFrom); + console.log("Transfer(s):", ghost_actualTransfer); + console.log("Enable Rebase:", ghost_enableRebase); + console.log("Disable Rebase:", ghost_disableRebase); + console.log("TransferFrom(s):", ghost_actualTransferFrom); + } + + function __mint(address addr, uint256 amount) internal { + (bool success, ) = address(usdm).call( + abi.encodeWithSignature("mintTokens(address,uint256)", addr, amount) + ); + assert(success); + } +} diff --git a/test/Invariant/LibAddressSet.sol b/test/Invariant/LibAddressSet.sol new file mode 100644 index 0000000..147032e --- /dev/null +++ b/test/Invariant/LibAddressSet.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.20; +import {console2 as console} from "forge-std/Test.sol"; + +struct AddressSet { + address[] addrs; + mapping(address => bool) saved; +} + +library LibAddressSet { + function add(AddressSet storage s, address addr) internal { + if (!s.saved[addr]) { + s.addrs.push(addr); + s.saved[addr] = true; + } + } + + function rand( + AddressSet storage s, + uint256 seed + ) internal view returns (address) { + if (s.addrs.length > 0) { + return s.addrs[seed % s.addrs.length]; + } else { + return address(0); + } + } + + function reduce( + AddressSet storage s, + uint256 acc, + function(uint256, address) external returns (uint256) func + ) internal returns (uint256) { + for (uint256 i; i < s.addrs.length; ++i) { + acc = func(acc, s.addrs[i]); + } + return acc; + } + + function forEach( + AddressSet storage s, + function(address) external func + ) internal { + for (uint256 i; i < s.addrs.length; ++i) { + func(s.addrs[i]); + } + } +} diff --git a/test/Invariant/USDM.sol b/test/Invariant/USDM.sol new file mode 100644 index 0000000..2d9dad0 --- /dev/null +++ b/test/Invariant/USDM.sol @@ -0,0 +1,359 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +contract USDM { + // Token name + string private _name; + // Token Symbol + string private _symbol; + // Total token shares + uint256 private _totalShares; + // Base value for rewardMultiplier + uint256 private constant _BASE = 1e18; + /** + * @dev rewardMultiplier represents a coefficient used in reward calculation logic. + * The value is represented with 18 decimal places for precision. + */ + uint256 public rewardMultiplier; + + // Mapping of shares per address + mapping(address => uint256) private _shares; + // Mapping of block status per address + mapping(address => bool) private _blocklist; + // Mapping of allowances per owner and spender + mapping(address => mapping(address => uint256)) private _allowances; + + // Events + event AccountBlocked(address indexed addr); + event AccountUnblocked(address indexed addr); + event RewardMultiplier(uint256 indexed value); + + /** + * Standard ERC20 Errors + * @dev See https://eips.ethereum.org/EIPS/eip-6093 + */ + error ERC20InsufficientBalance( + address sender, + uint256 shares, + uint256 sharesNeeded + ); + error ERC20InvalidSender(address sender); + error ERC20InvalidReceiver(address receiver); + error ERC20InsufficientAllowance( + address spender, + uint256 allowance, + uint256 needed + ); + error ERC20InvalidApprover(address approver); + error ERC20InvalidSpender(address spender); + // ERC2612 Errors + error ERC2612ExpiredDeadline(uint256 deadline, uint256 blockTimestamp); + error ERC2612InvalidSignature(address owner, address spender); + // USDM Errors + error USDMInvalidMintReceiver(address receiver); + error USDMInvalidBurnSender(address sender); + error USDMInsufficientBurnBalance( + address sender, + uint256 shares, + uint256 sharesNeeded + ); + error USDMInvalidRewardMultiplier(uint256 rewardMultiplier); + error USDMBlockedSender(address sender); + error USDMInvalidBlockedAccount(address account); + + constructor() { + _name = "USBM"; + _symbol = "US"; + _setRewardMultiplier(_BASE); + } + + function name() external view returns (string memory) { + return _name; + } + + function symbol() external view returns (string memory) { + return _symbol; + } + + function decimals() external pure returns (uint8) { + return 18; + } + + function convertToShares(uint256 amount) public view returns (uint256) { + return (amount * _BASE) / rewardMultiplier; + } + + function convertToTokens(uint256 shares) public view returns (uint256) { + return (shares * rewardMultiplier) / _BASE; + } + + function totalShares() external view returns (uint256) { + return _totalShares; + } + + function totalSupply() external view returns (uint256) { + return convertToTokens(_totalShares); + } + + function sharesOf(address account) public view returns (uint256) { + return _shares[account]; + } + + function balanceOf(address account) external view returns (uint256) { + return convertToTokens(sharesOf(account)); + } + + function _mint(address to, uint256 amount) private { + if (to == address(0)) { + revert USDMInvalidMintReceiver(to); + } + + _beforeTokenTransfer(address(0), to, amount); + + uint256 shares = convertToShares(amount); + _totalShares += shares; + + unchecked { + // Overflow not possible: shares + shares amount is at most totalShares + shares amount + // which is checked above. + _shares[to] += shares; + } + + _afterTokenTransfer(address(0), to, amount); + } + + function mintTokens(address to, uint256 amount) external { + _mint(to, amount); + } + + function _burn(address account, uint256 amount) private { + if (account == address(0)) { + revert USDMInvalidBurnSender(account); + } + + _beforeTokenTransfer(account, address(0), amount); + + uint256 shares = convertToShares(amount); + uint256 accountShares = sharesOf(account); + + if (accountShares < shares) { + revert USDMInsufficientBurnBalance(account, accountShares, shares); + } + + unchecked { + _shares[account] = accountShares - shares; + // Overflow not possible: amount <= accountShares <= totalShares. + _totalShares -= shares; + } + + _afterTokenTransfer(account, address(0), amount); + } + + function burn(address from, uint256 amount) external { + _burn(from, amount); + } + + function _beforeTokenTransfer( + address from, + address /* to */, + uint256 /* amount */ + ) private view { + // Each blocklist check is an SLOAD, which is gas intensive. + // We only block sender not receiver, so we don't tax every user + if (isBlocked(from)) { + revert USDMBlockedSender(from); + } + } + + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) private {} + + function _transfer(address from, address to, uint256 amount) private { + if (from == address(0)) { + revert ERC20InvalidSender(from); + } + if (to == address(0)) { + revert ERC20InvalidReceiver(to); + } + + _beforeTokenTransfer(from, to, amount); + + uint256 shares = convertToShares(amount); + uint256 fromShares = _shares[from]; + + if (fromShares < shares) { + revert ERC20InsufficientBalance(from, fromShares, shares); + } + + unchecked { + _shares[from] = fromShares - shares; + // Overflow not possible: the sum of all shares is capped by totalShares, and the sum is preserved by + // decrementing then incrementing. + _shares[to] += shares; + } + + _afterTokenTransfer(from, to, amount); + } + + function transfer(address to, uint256 amount) external returns (bool) { + address owner = msg.sender; + + _transfer(owner, to, amount); + + return true; + } + + function _blockAccount(address account) private { + if (isBlocked(account)) { + revert USDMInvalidBlockedAccount(account); + } + + _blocklist[account] = true; + emit AccountBlocked(account); + } + + function _unblockAccount(address account) private { + if (!isBlocked(account)) { + revert USDMInvalidBlockedAccount(account); + } + + _blocklist[account] = false; + emit AccountUnblocked(account); + } + + function blockAccounts(address[] calldata addresses) external { + for (uint256 i = 0; i < addresses.length; i++) { + _blockAccount(addresses[i]); + } + } + + function unblockAccounts(address[] calldata addresses) external { + for (uint256 i = 0; i < addresses.length; i++) { + _unblockAccount(addresses[i]); + } + } + + function isBlocked(address account) public view returns (bool) { + return _blocklist[account]; + } + + function _setRewardMultiplier(uint256 _rewardMultiplier) private { + if (_rewardMultiplier < _BASE) { + revert USDMInvalidRewardMultiplier(_rewardMultiplier); + } + + rewardMultiplier = _rewardMultiplier; + + emit RewardMultiplier(rewardMultiplier); + } + + function setRewardMultiplier(uint256 _rewardMultiplier) external { + _setRewardMultiplier(_rewardMultiplier); + } + + function addRewardMultiplier(uint256 _rewardMultiplierIncrement) external { + if (_rewardMultiplierIncrement == 0) { + revert USDMInvalidRewardMultiplier(_rewardMultiplierIncrement); + } + + _setRewardMultiplier(rewardMultiplier + _rewardMultiplierIncrement); + } + + function _approve(address owner, address spender, uint256 amount) private { + if (owner == address(0)) { + revert ERC20InvalidApprover(owner); + } + if (spender == address(0)) { + revert ERC20InvalidSpender(spender); + } + + _allowances[owner][spender] = amount; + } + + function approve(address spender, uint256 amount) external returns (bool) { + address owner = msg.sender; + + _approve(owner, spender, amount); + + return true; + } + + function allowance( + address owner, + address spender + ) public view returns (uint256) { + return _allowances[owner][spender]; + } + + function _spendAllowance( + address owner, + address spender, + uint256 amount + ) private { + uint256 currentAllowance = allowance(owner, spender); + + if (currentAllowance != type(uint256).max) { + if (currentAllowance < amount) { + revert ERC20InsufficientAllowance( + spender, + currentAllowance, + amount + ); + } + + unchecked { + _approve(owner, spender, currentAllowance - amount); + } + } + } + + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool) { + address spender = msg.sender; + + _spendAllowance(from, spender, amount); + _transfer(from, to, amount); + + return true; + } + + function increaseAllowance( + address spender, + uint256 addedValue + ) external returns (bool) { + address owner = msg.sender; + + _approve(owner, spender, allowance(owner, spender) + addedValue); + + return true; + } + + function decreaseAllowance( + address spender, + uint256 subtractedValue + ) external returns (bool) { + address owner = msg.sender; + uint256 currentAllowance = allowance(owner, spender); + + if (currentAllowance < subtractedValue) { + revert ERC20InsufficientAllowance( + spender, + currentAllowance, + subtractedValue + ); + } + + unchecked { + _approve(owner, spender, currentAllowance - subtractedValue); + } + + return true; + } +} diff --git a/test/Invariant/USTBInvariants.t.sol b/test/Invariant/USTBInvariants.t.sol new file mode 100644 index 0000000..d0fd32e --- /dev/null +++ b/test/Invariant/USTBInvariants.t.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.20; + +import {USDM} from "./USDM.sol"; +import {USTB} from "../../src/USTB.sol"; +import {Handler, RebaseTokenMath} from "./Handler.sol"; +import {Test, console2 as console} from "forge-std/Test.sol"; +import {LZEndpointMock} from "@layerzerolabs/contracts/lzApp/mocks/LZEndpointMock.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract USTBInvariants is Test { + using RebaseTokenMath for uint256; + + USTB public ustb; + USTB ustbChild; + USDM public usdm; + Handler public handler; + + function setUp() public { + uint16 mainChainId = uint16(block.chainid); + uint16 sideChainId = mainChainId + 1; + + usdm = new USDM(); + + LZEndpointMock lzEndpoint = new LZEndpointMock(mainChainId); + ustb = new USTB(address(usdm), mainChainId, address(lzEndpoint)); + + vm.chainId(sideChainId); + + USTB child = new USTB(address(usdm), mainChainId, address(lzEndpoint)); + + vm.chainId(mainChainId); + + ERC1967Proxy mainProxy = new ERC1967Proxy( + address(ustb), + abi.encodeWithSelector(USTB.initialize.selector, address(2)) + ); + + ustb = USTB(address(mainProxy)); + + vm.chainId(sideChainId); + + ERC1967Proxy childProxy = new ERC1967Proxy( + address(child), + abi.encodeWithSelector(USTB.initialize.selector, address(2)) + ); + + ustbChild = USTB(address(childProxy)); + + vm.chainId(mainChainId); + + lzEndpoint.setDestLzEndpoint(address(ustb), address(lzEndpoint)); + lzEndpoint.setDestLzEndpoint(address(ustbChild), address(lzEndpoint)); + + bytes memory ustbAddress = abi.encodePacked(uint160(address(ustb))); + bytes memory ustbChildAddress = abi.encodePacked( + uint160(address(ustbChild)) + ); + + ustb.setTrustedRemoteAddress(mainChainId, ustbChildAddress); + ustbChild.setTrustedRemoteAddress(mainChainId, ustbAddress); + + handler = new Handler(ustb, ustbChild, address(usdm)); + + bytes4[] memory selectors = new bytes4[](7); + + selectors[0] = Handler.mint.selector; + selectors[1] = Handler.burn.selector; + selectors[2] = Handler.disable.selector; + selectors[3] = Handler.transfer.selector; + selectors[4] = Handler.sendFrom.selector; + selectors[5] = Handler.transferFrom.selector; + selectors[6] = Handler.approve.selector; + + targetSelector( + FuzzSelector({addr: address(handler), selectors: selectors}) + ); + + targetContract(address(handler)); + } + + // The USTB contract's token balance should always be + // at least as much as the sum of individual mints. + function invariant_mint() public { + assertEq( + handler.ghost_mintedSum() - handler.ghost_burntSum(), + ustb.totalSupply() + ); + } + + // All to and fro bridging should be balanced out. + function invariant_bridgedToken() public { + assertEq( + handler.ghost_bridgedTokensTo() - handler.ghost_bridgedTokensFrom(), + 0 + ); + } + + // The USTB contract's token balance should always be + // at least as much as the sum of individual balances + function invariant_totalBalance() public { + uint256 sumOfBalances = handler.reduceActors(0, this.accumulateBalance); + assertEq(sumOfBalances, ustb.totalSupply()); + } + + // No individual account balance can exceed the USTB totalSupply(). + function invariant_userBalances() public { + handler.forEachActor(this.assertAccountBalanceLteTotalSupply); + } + + function assertAccountBalanceLteTotalSupply(address account) external { + assertLe(ustb.balanceOf(account), ustb.totalSupply()); + } + + function accumulateBalance( + uint256 balance, + address caller + ) external view returns (uint256) { + return balance + ustb.balanceOf(caller); + } + + function invariant_callSummary() public view { + handler.callSummary(); + } +} diff --git a/test/Invariant/report.md b/test/Invariant/report.md new file mode 100644 index 0000000..ad5e285 --- /dev/null +++ b/test/Invariant/report.md @@ -0,0 +1,361 @@ +--- +Author: c-n-o-t-e +Date: January 3, 2024 +--- + +# USTB Audit Report + +# Table of contents + +
+ +See table + +- [USTB Audit Report](#ustb-audit-report) +- [Table of contents](#table-of-contents) +- [Disclaimer](#disclaimer) +- [Risk Classification](#risk-classification) +- [Audit Details](#audit-details) + - [Scope](#scope) +- [Protocol Summary](#protocol-summary) + - [Roles](#roles) +- [Executive Summary](#executive-summary) + - [Issues found](#issues-found) + +
+
+ +# Disclaimer + +I make all effort to find as many vulnerabilities in the code in the given time period, but holds no responsibilities for the the findings provided in this document. A security audit by me is not an endorsement of the underlying business or product. The audit was time-boxed and the review of the code was solely on the security aspects of the solidity implementation of the contracts. + +**NOTE:** Gas optimization wasn't prioritize during testing. + +# Risk Classification + +| | | Impact | | | +| ---------- | ------ | ------ | ------ | --- | +| | | High | Medium | Low | +| | High | H | H/M | M | +| Likelihood | Medium | H/M | M | M/L | +| | Low | M | M/L | L | + +# Audit Details + +## Scope + +``` +src/ +--- USTB.sol + +lib/tangible-foundation-contracts/src/tokens +--- RebaseTokenUpgradeable.sol +--- LayerZeroRebaseTokenUpgradeable.sol +``` + +# Protocol Summary + +USTB extends the functionality of `LayerZeroRebaseTokenUpgradeable` to provide additional features specific to USTB. It adds capabilities for minting and burning tokens backed by an underlying asset, and dynamically updates the rebase index. + +# Executive Summary + +## Issues found + +| Severity | Number of issues found | +| -------- | ---------------------- | +| High | 1 | +| Medium | 1 | +| Low | 1 | + +# Findings + +## High + +### [H-1] `totalShares` not updated when tokens are transferred from a rebase user to a non-rebase user and vice-versa + +**Description:** In `RebaseTokenUpgradeable.sol`, when token transfer is done from a rebase user to a non-rebase user the shares to be transferred out isn't substracted from `totalShares` and when token transfer is done from a non-rebase user to a rebase user the shares to be transferred in isn't added to `totalShares` in the `_update()`. + +**Impact:** USTB `totalSupply()` is inaccurate. + +**Proof of Concept:** +The code below contains two tests: + +`test_ReturnWrongTotalSupplyAfterTokenTransferFromRebaseToNonRebase()` shows how alice a rebase user transfers tokens to bob a non-rebase user and the `totalShares` of rebase tokens isn't reduced by the transferred amount. + +`test_ReturnWrongTotalSupplyAfterTokenTransferFromNonRebaseToRebase()` shows how bob a non-rebase user transfers tokens to alice a rebase user and the transferred amount isn't added to the `totalShares` of non-rebase tokens. + +```javascript + + function test_ReturnWrongTotalSupplyAfterTokenTransferFromRebaseToNonRebase() + public + { + vm.startPrank(usdmHolder); + usdm.transfer(alice, 100e18); + usdm.transfer(bob, 100e18); + + vm.startPrank(alice); + usdm.approve(address(ustb), 100e18); + ustb.mint(alice, 100e18); + + vm.startPrank(bob); + usdm.approve(address(ustb), 100e18); + + ustb.disableRebase(bob, true); + ustb.mint(bob, 100e18); + + vm.roll(18349000); + vm.startPrank(usdmController); + + (bool success, ) = address(usdm).call( + abi.encodeWithSignature("addRewardMultiplier(uint256)", 134e12) + ); + assert(success); + + vm.startPrank(indexManager); + ustb.refreshRebaseIndex(); // force update + + vm.startPrank(alice); + uint256 balance1 = ustb.balanceOf(alice); + + //////////////////////////// Shows Bug //////////////////////////// + + console.log( + "Total supply before transferring tokens to bob", + ustb.totalSupply() + ); + + uint256 totalSupplyBeforeTransfer = ustb.totalSupply(); + ustb.transfer(bob, balance1); + uint256 totalSupplyAfterTransfer = ustb.totalSupply(); + + console.log( + "Total supply after transferring tokens to bob", + ustb.totalSupply() + ); + + // totalSupplyBeforeTransfer is meant to be equal to totalSupplyAfterTransfer + // because tokens are only transferred between users not burnt/minted. + assertLt(totalSupplyBeforeTransfer, totalSupplyAfterTransfer); + } + + function test_ReturnWrongTotalSupplyAfterTokenTransferFromNonRebaseToRebase() + public + { + vm.startPrank(usdmHolder); + usdm.transfer(bob, 100e18); + usdm.transfer(alice, 100e18); + + vm.startPrank(bob); + usdm.approve(address(ustb), 100e18); + + ustb.disableRebase(bob, true); + ustb.mint(bob, 100e18); + + vm.startPrank(alice); + usdm.approve(address(ustb), 100e18); + ustb.mint(alice, 100e18); + + vm.roll(18349000); + vm.startPrank(usdmController); + + (bool success, ) = address(usdm).call( + abi.encodeWithSignature("addRewardMultiplier(uint256)", 134e12) + ); + assert(success); + + vm.startPrank(indexManager); + ustb.refreshRebaseIndex(); // force update + + vm.startPrank(bob); + uint256 balance1 = ustb.balanceOf(bob); + + //////////////////////////// Shows Bug //////////////////////////// + + console.log( + "Total supply before transferring tokens to bob", + ustb.totalSupply() + ); + + uint256 totalSupplyBeforeTransfer = ustb.totalSupply(); + ustb.transfer(alice, balance1); + uint256 totalSupplyAfterTransfer = ustb.totalSupply(); + + console.log( + "Total supply after transferring tokens to bob", + ustb.totalSupply() + ); + + // totalSupplyBeforeTransfer is meant to be equal to totalSupplyAfterTransfer + // because tokens are only transferred between users not burnt/minted. + assertLt(totalSupplyAfterTransfer, totalSupplyBeforeTransfer); + } +``` + +To `run` add to `USTB.t.sol`. + +**Recommended Mitigation:** + +After line 231 in [RebaseTokenUpgradeable.sol](https://github.com/TangibleTNFT/tangible-foundation-contracts/blob/c98ea3cb772c8c3939527be5fd1ebe21ce7e9cc3/src/tokens/RebaseTokenUpgradeable.sol#L231) + +```diff ++ if (optOutTo && to != address(0)) $.totalShares -= shares; +``` + +After line 252 in [RebaseTokenUpgradeable.sol](https://github.com/TangibleTNFT/tangible-foundation-contracts/blob/c98ea3cb772c8c3939527be5fd1ebe21ce7e9cc3/src/tokens/RebaseTokenUpgradeable.sol#L252) + +```diff ++ if (optOutFrom) $.totalShares += shares; +``` + +## Medium + +### [M-1] Amount newly minted to non-rebase users are not checked for `totalSupply` overflow + +**Description:** In `RebaseTokenUpgradeable.sol`, when a non-rebase user mints tokens the `_update()` does not check if the addition of minted `amount` plus `totalShares` plus `ERC20Upgradeable.totalSupply()` overflows. + +**Impact:** When this happens calls to `RebaseTokenUpgradeable.totalSupply()` overflows thereby making `totalSupply() unreachable`. + +**Proof of Concept:** +The code below contains one test: + +`test_TotalSupplyUnreachableWhenNonRebaseMintsTokenAboveSupply()` shows how `totalSupply()` overflows. + +```Javascript + // To detail this error I exposed the `_mint()` + + // Add to USTB.sol + function exposedMintForTesting(address to, uint256 amount) external mainChain(true) { + _mint(to, amount); + } + + // Test + function test_TotalSupplyUnreachableWhenNonRebaseMintsTokenAboveSupply() + public + { + address usdmMinter = 0x48AEB395FB0E4ff8433e9f2fa6E0579838d33B62; + vm.startPrank(usdmMinter); + + (bool success, ) = address(usdm).call( + abi.encodeWithSignature("mint(address,uint256)", address(3), 1e18) + ); + + assert(success); + + // Mints 1e18 rebase tokens to address(3) + vm.startPrank(address(3)); + usdm.approve(address(ustb), type(uint256).max); + ustb.mint(address(3), 1e18); + + // Mints max of uint256 rebase tokens to address(7) + vm.startPrank(address(7)); + usdm.approve(address(ustb), type(uint256).max); + ustb.disableRebase(address(7), true); + ustb.exposedMintForTesting(address(7), type(uint256).max); + + //////////////////////////// Fails with overflow //////////////////////////// + + // 1e18 + type(uint256).max which overflows + vm.expectRevert(); + ustb.totalSupply(); + } +``` + +To `run` add to `USTB.t.sol`. + +**Recommended Mitigation:** + +After line 245 in [RebaseTokenUpgradeable.sol](https://github.com/TangibleTNFT/tangible-foundation-contracts/blob/c98ea3cb772c8c3939527be5fd1ebe21ce7e9cc3/src/tokens/RebaseTokenUpgradeable.sol#L245) + +```diff ++ _checkTotalSupplyOverFlow(amount); + + ....................... + ++ error SupplyOverflow(); + + ....................... + ++ function _checkTotalSupplyOverFlow(uint256 amount) private view { ++ unchecked { ++ if (amount + totalSupply() < totalSupply()) { ++ revert SupplyOverflow(); ++ } ++ } ++ } +``` + +## Low + +### [L-1] Fails to bridge tokens for non-rebase users + +**Description:** In `LayerZeroRebaseTokenUpgradeable.sol`, when a non-rebase user tries to bridge tokens it fails because when `_debitFrom()` is called `_transferableShares()` gets called as well which is solely used to check rebase user balance before tranferring tokens, given the user trying to bridge token is a non-rebase it fails stating `AmountExceedsBalance()` + +**Impact:** Fails everytime a non-rebase users tries to bridge tokens. + +**Proof of Concept:** +The code below contains one test: + +`test_shouldFailWhenSenderIsNonRebaseUser()` shows how a non-rebase user fails to bridge tokens. + +```Javascript + error AmountExceedsBalance( + address account, + uint256 balance, + uint256 amount + ); + + function test_shouldFailWhenSenderIsNonRebaseUser() public { + vm.startPrank(usdmHolder); + usdm.approve(address(ustb), 1e18); + + // user becomes non-rebase + ustb.disableRebase(usdmHolder, true); + ustb.mint(usdmHolder, 1e18); + + uint256 nativeFee; + (nativeFee, ) = ustb.estimateSendFee( + uint16(block.chainid), + abi.encodePacked(alice), + 0.5e18, + false, + "" + ); + + // Catch AmountExceedsBalance error. + vm.expectRevert( + abi.encodeWithSelector( + AmountExceedsBalance.selector, + usdmHolder, + 0, + 0.5e18 + ) + ); + + ustb.sendFrom{value: (nativeFee * 105) / 100}( + usdmHolder, + uint16(block.chainid), + abi.encodePacked(alice), + 0.5e18, + payable(usdmHolder), + address(0), + "" + ); + } +``` + +To `run` add to `USTB.t.sol`. + +**Recommended Mitigation:** + +If only rebase user are allowed to bridge tokens, then after line 154 in [LayerZeroRebaseTokenUpgradeable.sol](https://github.com/TangibleTNFT/tangible-foundation-contracts/blob/c98ea3cb772c8c3939527be5fd1ebe21ce7e9cc3/src/tokens/LayerZeroRebaseTokenUpgradeable.sol#L154) + +```diff ++ error OnlyRebaseTokensCanBeBridged(); + ................................ + ++ if (_isRebaseDisabled(from)) { ++ revert OnlyRebaseTokensCanBeBridged(); ++ } +``` + +If both rebase and non-rebase user are allowed to bridge tokens then the logic in `_debitFrom()` needs to be rewritten. diff --git a/test/USTB.t.sol b/test/USTB.t.sol index 007eb31..0bd039b 100644 --- a/test/USTB.t.sol +++ b/test/USTB.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Unlicense pragma solidity ^0.8.13; -import "forge-std/Test.sol"; +import {Test} from "forge-std/Test.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; @@ -11,6 +11,15 @@ import "@layerzerolabs/contracts/lzApp/mocks/LZEndpointMock.sol"; import "src/USTB.sol"; contract USTBTest is Test { + error NotAuthorized(address caller); + error ValueUnchanged(); + error ERC20InsufficientAllowance( + address spender, + uint256 allowance, + uint256 needed + ); + error InvalidZeroAddress(); + USTB ustb; USTB ustbChild; @@ -51,14 +60,18 @@ contract USTBTest is Test { usdm = IERC20(main.UNDERLYING()); - ERC1967Proxy mainProxy = - new ERC1967Proxy(address(main), abi.encodeWithSelector(USTB.initialize.selector, indexManager)); + ERC1967Proxy mainProxy = new ERC1967Proxy( + address(main), + abi.encodeWithSelector(USTB.initialize.selector, indexManager) + ); ustb = USTB(address(mainProxy)); vm.chainId(sideChainId); - ERC1967Proxy childProxy = - new ERC1967Proxy(address(child), abi.encodeWithSelector(USTB.initialize.selector, indexManager)); + ERC1967Proxy childProxy = new ERC1967Proxy( + address(child), + abi.encodeWithSelector(USTB.initialize.selector, indexManager) + ); ustbChild = USTB(address(childProxy)); vm.chainId(mainChainId); @@ -69,7 +82,9 @@ contract USTBTest is Test { lzEndpoint.setDestLzEndpoint(address(ustbChild), address(lzEndpoint)); bytes memory ustbAddress = abi.encodePacked(uint160(address(ustb))); - bytes memory ustbChildAddress = abi.encodePacked(uint160(address(ustbChild))); + bytes memory ustbChildAddress = abi.encodePacked( + uint160(address(ustbChild)) + ); ustb.setTrustedRemoteAddress(mainChainId, ustbChildAddress); ustbChild.setTrustedRemoteAddress(mainChainId, ustbAddress); @@ -85,8 +100,11 @@ contract USTBTest is Test { USTB instance2 = new USTB(usdmAddress, mainChainId, address(1)); - bytes32 slot = keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) - & ~bytes32(uint256(0xff)); + bytes32 slot = keccak256( + abi.encode( + uint256(keccak256("openzeppelin.storage.Initializable")) - 1 + ) + ) & ~bytes32(uint256(0xff)); vm.store(address(instance1), slot, 0); vm.store(address(instance2), slot, 0); @@ -147,7 +165,9 @@ contract USTBTest is Test { vm.roll(18349000); vm.startPrank(usdmController); - (bool success,) = address(usdm).call(abi.encodeWithSignature("addRewardMultiplier(uint256)", 134e12)); + (bool success, ) = address(usdm).call( + abi.encodeWithSignature("addRewardMultiplier(uint256)", 134e12) + ); assert(success); vm.startPrank(indexManager); @@ -171,7 +191,9 @@ contract USTBTest is Test { vm.roll(18349000); vm.startPrank(usdmController); - (bool success,) = address(usdm).call(abi.encodeWithSignature("addRewardMultiplier(uint256)", 134e12)); + (bool success, ) = address(usdm).call( + abi.encodeWithSignature("addRewardMultiplier(uint256)", 134e12) + ); assert(success); vm.startPrank(indexManager); @@ -190,7 +212,9 @@ contract USTBTest is Test { vm.roll(18350000); vm.startPrank(usdmController); - (success,) = address(usdm).call(abi.encodeWithSignature("addRewardMultiplier(uint256)", 134e12)); + (success, ) = address(usdm).call( + abi.encodeWithSignature("addRewardMultiplier(uint256)", 134e12) + ); assert(success); vm.startPrank(indexManager); @@ -209,18 +233,35 @@ contract USTBTest is Test { ustb.mint(usdmHolder, 1e18); uint256 nativeFee; - (nativeFee,) = ustb.estimateSendFee(uint16(block.chainid), abi.encodePacked(alice), 0.5e18, false, ""); - ustb.sendFrom{value: nativeFee * 105 / 100}( - usdmHolder, uint16(block.chainid), abi.encodePacked(alice), 0.5e18, payable(usdmHolder), address(0), "" + (nativeFee, ) = ustb.estimateSendFee( + uint16(block.chainid), + abi.encodePacked(alice), + 0.5e18, + false, + "" + ); + ustb.sendFrom{value: (nativeFee * 105) / 100}( + usdmHolder, + uint16(block.chainid), + abi.encodePacked(alice), + 0.5e18, + payable(usdmHolder), + address(0), + "" ); assertApproxEqAbs(ustb.balanceOf(usdmHolder), 0.5e18, 2); assertApproxEqAbs(ustbChild.balanceOf(alice), 0.5e18, 2); vm.startPrank(alice); - (nativeFee,) = ustb.estimateSendFee( - uint16(block.chainid), abi.encodePacked(usdmHolder), ustbChild.balanceOf(alice), false, "" + + (nativeFee, ) = ustb.estimateSendFee( + uint16(block.chainid), + abi.encodePacked(usdmHolder), + ustbChild.balanceOf(alice), + false, + "" ); - ustbChild.sendFrom{value: nativeFee * 105 / 100}( + ustbChild.sendFrom{value: (nativeFee * 105) / 100}( alice, uint16(block.chainid), abi.encodePacked(usdmHolder), @@ -248,7 +289,9 @@ contract USTBTest is Test { vm.roll(18349000); vm.startPrank(usdmController); - (bool success,) = address(usdm).call(abi.encodeWithSignature("addRewardMultiplier(uint256)", 134e12)); + (bool success, ) = address(usdm).call( + abi.encodeWithSignature("addRewardMultiplier(uint256)", 134e12) + ); assert(success); vm.startPrank(indexManager); @@ -278,7 +321,9 @@ contract USTBTest is Test { vm.roll(18349000); vm.startPrank(usdmController); - (bool success,) = address(usdm).call(abi.encodeWithSignature("addRewardMultiplier(uint256)", 134e12)); + (bool success, ) = address(usdm).call( + abi.encodeWithSignature("addRewardMultiplier(uint256)", 134e12) + ); assert(success); vm.startPrank(indexManager); @@ -309,7 +354,9 @@ contract USTBTest is Test { vm.roll(18349000); vm.startPrank(usdmController); - (bool success,) = address(usdm).call(abi.encodeWithSignature("addRewardMultiplier(uint256)", 134e12)); + (bool success, ) = address(usdm).call( + abi.encodeWithSignature("addRewardMultiplier(uint256)", 134e12) + ); assert(success); vm.startPrank(indexManager); @@ -341,7 +388,9 @@ contract USTBTest is Test { vm.roll(18349000); vm.startPrank(usdmController); - (bool success,) = address(usdm).call(abi.encodeWithSignature("addRewardMultiplier(uint256)", 134e12)); + (bool success, ) = address(usdm).call( + abi.encodeWithSignature("addRewardMultiplier(uint256)", 134e12) + ); assert(success); vm.startPrank(indexManager); @@ -354,4 +403,73 @@ contract USTBTest is Test { assertEq(ustb.balanceOf(alice), 0); assertApproxEqAbs(ustb.balanceOf(bob), balance + balance, 1); } + + /////////////////////////////// NEW TEST /////////////////////////////////// + + function test_shouldFailTodisableRebaseIfCallerIsNotAuthorized() public { + vm.startPrank(usdmHolder); + usdm.approve(address(ustb), 1e18); + ustb.isNotRebase(usdmHolder); + + ustb.mint(usdmHolder, 1e18); + vm.stopPrank(); + + vm.expectRevert( + abi.encodeWithSelector(NotAuthorized.selector, address(this)) + ); + + ustb.disableRebase(usdmHolder, true); + } + + function test_shouldFailToDisableRebaseIfValueIsUnchanged() public { + vm.startPrank(usdmHolder); + usdm.approve(address(ustb), 1e18); + + ustb.mint(usdmHolder, 1e18); + vm.expectRevert(abi.encodeWithSelector(ValueUnchanged.selector)); + + ustb.disableRebase(usdmHolder, false); + } + + function test_burnViaApprovedAddress() public { + vm.startPrank(usdmHolder); + usdm.approve(address(ustb), 1e18); + + ustb.mint(usdmHolder, 1e18); + ustb.approve(address(this), 1e18); + + vm.stopPrank(); + ustb.burn(usdmHolder, ustb.balanceOf(usdmHolder)); + + assertEq(ustb.balanceOf(usdmHolder), 0); + assertEq(ustb.totalSupply(), 0); + } + + function test_failToBurnTokenFromNotApprovedOrOwner() public { + vm.startPrank(usdmHolder); + + usdm.approve(address(ustb), 1e18); + ustb.mint(usdmHolder, 1e18); + + ustb.approve(address(this), 1e18); + vm.stopPrank(); + + ustb.burn(usdmHolder, ustb.balanceOf(usdmHolder)); + + assertEq(ustb.balanceOf(usdmHolder), 0); + assertEq(ustb.totalSupply(), 0); + } + + function test_shouldFailToSetRebaseIndex() public { + vm.expectRevert( + abi.encodeWithSelector(NotAuthorized.selector, deployer) + ); + ustbChild.setRebaseIndex(1e18, 1); + assertEq(ustbChild.rebaseIndex(), 1e18); + } + + function test_shouldFailTosetRebaseIndexManager() public { + vm.expectRevert(abi.encodeWithSelector(InvalidZeroAddress.selector)); + ustb.setRebaseIndexManager(address(0)); + } }