diff --git a/contracts/README.md b/contracts/README.md index be4ad08a..f558c384 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -111,6 +111,26 @@ To integrate with the NobleDollar contract: 3. **Yield Claims**: Call `yield(address)` to check claimable amounts and `claim()` to collect yield 4. **Cross-Chain Operations**: Utilize Hyperlane's routing for cross-chain transfers +## Rounding Behavior and Principal-Balance Relationship + +### Mathematical Precision Considerations + +Due to the dual accounting system that tracks both principal and balance, there can be slight rounding inconsistencies in how these values relate to each other depending on how a user's position was created: + +- **Organic yield accrual**: When users hold tokens while the index increases, their balance is calculated as `roundDown(principal × index)` +- **Direct minting**: When users mint tokens at the current index, their principal is calculated as `roundDown(balance / index)`, which can result in `balance ≈ roundUp(principal × index)` + +#### Example +At index 1.199e12: +- A user who held 100 principal from index 1e12 will have balance = 119 wei (`roundDown(100 × 1.199)`) +- A user who mints 119 wei will receive 99 principal (`roundDown(119 / 1.199)`), where `99 × 1.199 = 118.8 → 119` + +This means users with identical balances may have slightly different principal amounts (and thus different future yield accrual rates) depending on their transaction history. These differences are typically limited to 1 wei of principal and are an inherent characteristic of maintaining separate principal and balance values with integer arithmetic. + +### Impact +- The rounding differences are minimal and do not affect the security or core functionality of the protocol +- Users will always receive at least the yield they are entitled to based on their principal + ## License Copyright 2025 NASD Inc. All rights reserved. diff --git a/contracts/src/NobleDollar.sol b/contracts/src/NobleDollar.sol index 3ba04c49..5073d93c 100644 --- a/contracts/src/NobleDollar.sol +++ b/contracts/src/NobleDollar.sol @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -pragma solidity 0.8.20; +pragma solidity 0.8.30; import {HypERC20} from "@hyperlane/token/HypERC20.sol"; @@ -85,7 +85,9 @@ contract NobleDollar is HypERC20 { } } - constructor(address mailbox_) HypERC20(6, 1, mailbox_) {} + constructor(address mailbox_) HypERC20(6, 1, mailbox_) { + _disableInitializers(); + } function initialize(address hook_, address ism_) public virtual initializer { super.initialize("Noble Dollar", "USDN", hook_, ism_, msg.sender); @@ -142,7 +144,8 @@ contract NobleDollar is HypERC20 { * @custom:emits YieldClaimed when yield is successfully claimed. */ function claim() public { - uint256 amount = yield(msg.sender); + // Avoid DOS claiming by taking min of the contract's balance and user's yield. + uint256 amount = UIntMath.min256(balanceOf(address(this)), yield(msg.sender)); if (amount == 0) revert NoClaimableYield(); @@ -187,6 +190,8 @@ contract NobleDollar is HypERC20 { // Distribute yield, derive new index from the adjusted total supply. // NOTE: We don't want to perform any principal updates in the case of yield accrual. if (to == address(this)) { + if ($.totalPrincipal == 0) return; + uint128 oldIndex = $.index; $.index = IndexingMath.getIndexRoundedDown(totalSupply(), $.totalPrincipal); diff --git a/contracts/test/NobleDollar.t.sol b/contracts/test/NobleDollar.t.sol index 7c88c025..543c33ca 100644 --- a/contracts/test/NobleDollar.t.sol +++ b/contracts/test/NobleDollar.t.sol @@ -18,7 +18,6 @@ pragma solidity >=0.8.0; import {Test} from "forge-std/Test.sol"; -import {console} from "forge-std/console.sol"; import {NoopIsm} from "@hyperlane/isms/NoopIsm.sol"; import {Message} from "@hyperlane/libs/Message.sol"; @@ -64,7 +63,7 @@ contract NobleDollarTest is Test { (bool mintSuccess,) = MAILBOX.call(mintPayload); // ASSERT: The transfer was successful, USER1 has a balance of 1M $USDN with a principal of 1M. - assertEq(mintSuccess, true); + assertTrue(mintSuccess, "Initial mint should succeed"); assertEq(usdn.index(), 1e12); assertEq(usdn.totalSupply(), 1e12); @@ -85,7 +84,7 @@ contract NobleDollarTest is Test { (bool yieldSuccess,) = MAILBOX.call(yieldPayload); // ASSERT: The yield accrual was successful, USER1 has 111.506849 $USDN of claimable yield. - assertEq(yieldSuccess, true); + assertTrue(yieldSuccess, "Yield accrual should succeed"); assertEq(usdn.index(), 1000111506849); assertEq(usdn.totalSupply(), 1000111506849); @@ -136,7 +135,7 @@ contract NobleDollarTest is Test { (bool yieldSuccess2,) = MAILBOX.call(yieldPayload2); // ASSERT: The yield accrual was successful. - assertEq(yieldSuccess2, true); + assertTrue(yieldSuccess2, "Second yield accrual should succeed"); assertEq(usdn.index(), 1000223013698); assertEq(usdn.totalSupply(), 1000223013698); @@ -148,4 +147,373 @@ contract NobleDollarTest is Test { assertEq(usdn.principalOf(USER2), 499944252792); assertEq(usdn.yield(USER2), 55747208); } + + function test_transferToUSDNFromNonZeroAccountReverts() public { + // ACT: Transfer of 1M $USDN from Noble Core to USER1. + bytes memory mintPayload = abi.encodeWithSignature( + "process(bytes,bytes)", + 0x0, + hex"03000000004e4f424c726f757465725f6170700000000000000000000000000001000000000000000000000001000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000000000e8d4a51000" + ); + (bool mintSuccess,) = MAILBOX.call(mintPayload); + assertTrue(mintSuccess, "Initial mint should succeed"); + + uint256 _user1Balance = usdn.balanceOf(USER1); + + assertEq(_user1Balance, 1000000e6, "user 1 should have 1 million usdn"); + + vm.expectRevert(abi.encodeWithSelector(NobleDollar.InvalidTransfer.selector)); + + vm.prank(USER1); + usdn.transfer(address(usdn), 1000e6); + } + + function test_transferFromToUSDNFromNonZeroAccountReverts() public { + // ACT: Transfer of 1M $USDN from Noble Core to USER1. + bytes memory mintPayload = abi.encodeWithSignature( + "process(bytes,bytes)", + 0x0, + hex"03000000004e4f424c726f757465725f6170700000000000000000000000000001000000000000000000000001000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000000000e8d4a51000" + ); + (bool mintSuccess,) = MAILBOX.call(mintPayload); + assertTrue(mintSuccess, "Initial mint should succeed"); + + uint256 _user1Balance = usdn.balanceOf(USER1); + + assertEq(_user1Balance, 1000000e6, "user 1 should have 1 million usdn"); + + vm.prank(USER1); + usdn.approve(USER2, type(uint256).max); + + vm.expectRevert(abi.encodeWithSelector(NobleDollar.InvalidTransfer.selector)); + + vm.prank(USER2); + usdn.transferFrom(USER1, address(usdn), 1000e6); + } + + function test_noClaimableYield() public { + // Test when timestamp has not progressed from mint so claimable yield should revert + bytes memory mintPayload = abi.encodeWithSignature( + "process(bytes,bytes)", + 0x0, + hex"03000000004e4f424c726f757465725f6170700000000000000000000000000001000000000000000000000001000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000000000e8d4a51000" + ); + (bool mintSuccess,) = MAILBOX.call(mintPayload); + assertTrue(mintSuccess, "Initial mint should succeed"); + + vm.expectRevert(NobleDollar.NoClaimableYield.selector); + + vm.prank(USER1); + usdn.claim(); + + // Test when account with zero balance calls claim() + vm.expectRevert(NobleDollar.NoClaimableYield.selector); + + vm.prank(USER2); + usdn.claim(); + } + + function test_uint112MaxMint() public { + // Test when timestamp has not progressed from mint so claimable yield should revert + bytes memory mintPayload = abi.encodeWithSignature( + "process(bytes,bytes)", + 0x0, + hex"03000000004e4f424c726f757465725f6170700000000000000000000000000001000000000000000000000001000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000ffffffffffffffffffffffffffff" + ); + (bool mintSuccess,) = MAILBOX.call(mintPayload); + assertTrue(mintSuccess, "Initial mint should succeed"); + + uint256 _user1Balance = usdn.balanceOf(USER1); + + assertEq(_user1Balance, type(uint112).max, "user 1 should have max usdn"); + + // Test when timestamp has not progressed from mint so claimable yield should revert + mintPayload = abi.encodeWithSignature( + "process(bytes,bytes)", + 0x0, + hex"03000000004e4f424c726f757465725f6170700000000000000000000000000001000000000000000000000001000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000000000000000001" + ); + (mintSuccess,) = MAILBOX.call(mintPayload); + + assertEq(mintSuccess, false, "minting more than uint112 max should fail"); + } + + function test_secondDepositPostYieldReceivesCorrectIndex() public { + // Mint 1 million to USER1 + bytes memory mintPayload = abi.encodeWithSignature( + "process(bytes,bytes)", + 0x0, + hex"03000000004e4f424c726f757465725f6170700000000000000000000000000001000000000000000000000001000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000000000e8d4a51000" + ); + (bool mintSuccess,) = MAILBOX.call(mintPayload); + assertTrue(mintSuccess, "Initial mint should succeed"); + + // Accrue 1 million in yield + bytes memory yieldPayload = abi.encodeWithSignature( + "process(bytes,bytes)", + 0x0, + hex"03000000014e4f424c726f757465725f6170700000000000000000000000000001000000000000000000000001000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000000000000000000000000000000000e8d4a51000" + ); + (bool yieldSuccess,) = MAILBOX.call(yieldPayload); + assertTrue(yieldSuccess, "Yield accrual should succeed"); + + // Mint 1 million to USER2 + mintPayload = abi.encodeWithSignature( + "process(bytes,bytes)", + 0x0, + hex"03000000004e4f424c726f757465725f6170700000000000000000000000000001000000000000000000000001000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000f2f1acbe0ba726fee8d75f3e32900526874740bb000000000000000000000000000000000000000000000000000000e8d4a51000" + ); + (mintSuccess,) = MAILBOX.call(mintPayload); + assertEq(mintSuccess, true); + + uint256 _principalUSER1 = usdn.principalOf(USER1); + uint256 _principalUSER2 = usdn.principalOf(USER2); + + assertEq(_principalUSER1, 1000000e6, "user 1 should have 1 million principal"); + assertEq(_principalUSER2, 500000e6, "user 2 should have 500 thousand principal"); + } + + function test_claimYield() public { + // Mint 1M USDN to USER1 + bytes memory mintPayload = abi.encodeWithSignature( + "process(bytes,bytes)", + 0x0, + hex"03000000004e4f424c726f757465725f6170700000000000000000000000000001000000000000000000000001000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000000000e8d4a51000" + ); + (bool mintSuccess,) = MAILBOX.call(mintPayload); + assertTrue(mintSuccess, "Initial mint should succeed"); + + // Verify initial state + assertEq(usdn.balanceOf(USER1), 1e12, "USER1 should have 1M USDN"); + assertEq(usdn.yield(USER1), 0, "USER1 should have no yield initially"); + + // Accrue yield equal to deposit (1M USDN - 100% yield) + bytes memory yieldPayload = abi.encodeWithSignature( + "process(bytes,bytes)", + 0x0, + hex"03000000014e4f424c726f757465725f6170700000000000000000000000000001000000000000000000000001000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000000000000000000000000000000000e8d4a51000" + ); + (bool yieldSuccess,) = MAILBOX.call(yieldPayload); + assertTrue(yieldSuccess, "Yield accrual should succeed"); + + // Verify yield is available + uint256 claimableYield = usdn.yield(USER1); + assertEq(claimableYield, 1e12, "USER1 should have 1M USDN in claimable yield"); + + // Claim yield + vm.expectEmit(true, true, true, true); + emit NobleDollar.YieldClaimed(USER1, 1e12); + + vm.prank(USER1); + usdn.claim(); + + // Verify post-claim state + assertEq(usdn.balanceOf(USER1), 2e12, "USER1 balance should be doubled (original + yield)"); + assertEq(usdn.balanceOf(address(usdn)), 0, "Contract balance should be zero after full claim"); + assertEq(usdn.yield(USER1), 0, "USER1 should have no claimable yield after claiming"); + assertEq(usdn.principalOf(USER1), 1e12, "USER1 principal should remain unchanged"); + assertEq(usdn.totalSupply(), 2e12, "Total supply should reflect the claimed yield"); + } + + function test_claimYieldMultipleUsers() public { + // Mint 1M to USER1 and 2M to USER2 + bytes memory mintPayload1 = abi.encodeWithSignature( + "process(bytes,bytes)", + 0x0, + hex"03000000004e4f424c726f757465725f6170700000000000000000000000000001000000000000000000000001000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000000000e8d4a51000" + ); + (bool mintSuccess1,) = MAILBOX.call(mintPayload1); + assertTrue(mintSuccess1); + + bytes memory mintPayload2 = abi.encodeWithSignature( + "process(bytes,bytes)", + 0x0, + hex"03000000004e4f424c726f757465725f6170700000000000000000000000000001000000000000000000000001000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000f2f1acbe0ba726fee8d75f3e32900526874740bb000000000000000000000000000000000000000000000000000001d1a94a2000" + ); + (bool mintSuccess2,) = MAILBOX.call(mintPayload2); + assertTrue(mintSuccess2); + + // Accrue yield equal to total deposits (3M USDN total yield) + bytes memory yieldPayload = abi.encodeWithSignature( + "process(bytes,bytes)", + 0x0, + hex"03000000014e4f424c726f757465725f6170700000000000000000000000000001000000000000000000000001000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000000000000000000000000000000002ba7def3000" + ); + (bool yieldSuccess,) = MAILBOX.call(yieldPayload); + assertTrue(yieldSuccess); + + // Both users should have yield equal to their deposits (100% yield) + uint256 user1Yield = usdn.yield(USER1); + uint256 user2Yield = usdn.yield(USER2); + + assertEq(user1Yield, 1e12, "USER1 should have 1M yield"); + assertEq(user2Yield, 2e12, "USER2 should have 2M yield"); + + // USER1 claims + vm.prank(USER1); + usdn.claim(); + assertEq(usdn.balanceOf(USER1), 2e12, "USER1 balance should be doubled"); + assertEq(usdn.yield(USER1), 0, "USER1 should have no yield after claiming"); + + // USER2 claims + vm.prank(USER2); + usdn.claim(); + assertEq(usdn.balanceOf(USER2), 4e12, "USER2 balance should be doubled"); + assertEq(usdn.yield(USER2), 0, "USER2 should have no yield after claiming"); + } + + function test_claimYieldAfterTransfer() public { + // Mint 1M to USER1 + bytes memory mintPayload = abi.encodeWithSignature( + "process(bytes,bytes)", + 0x0, + hex"03000000004e4f424c726f757465725f6170700000000000000000000000000001000000000000000000000001000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000000000e8d4a51000" + ); + (bool success,) = MAILBOX.call(mintPayload); + + // Accrue 1M yield (100% yield) + bytes memory yieldPayload = abi.encodeWithSignature( + "process(bytes,bytes)", + 0x0, + hex"03000000014e4f424c726f757465725f6170700000000000000000000000000001000000000000000000000001000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000000000000000000000000000000000e8d4a51000" + ); + (success,) = MAILBOX.call(yieldPayload); + + // USER1 should have 1M yield + assertEq(usdn.yield(USER1), 1e12, "USER1 should have 1M yield before transfer"); + + // Transfer half (500k) to USER2 + vm.prank(USER1); + usdn.transfer(USER2, 5e11); + + // Call yield + uint256 user1Yield = usdn.yield(USER1); + uint256 user2Yield = usdn.yield(USER2); + + assertEq(user1Yield, 1e12, "USER1 should have 1m of yield"); + assertEq(user2Yield, 0, "USER2 should have 0 yield"); + + // USER1 claims their yield + vm.prank(USER1); + usdn.claim(); + assertEq(usdn.balanceOf(USER1), 15e11, "USER1 should have 1.5M balance after claiming yield"); + + // USER2 yield claim should fail + vm.expectRevert(abi.encode(NobleDollar.NoClaimableYield.selector)); + vm.prank(USER2); + usdn.claim(); + + // Accrue another 1M yield (now distributed proportionally) + yieldPayload = abi.encodeWithSignature( + "process(bytes,bytes)", + 0x0, + hex"03000000024e4f424c726f757465725f6170700000000000000000000000000001000000000000000000000001000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000000000000000000000000000000000e8d4a51000" + ); + (success,) = MAILBOX.call(yieldPayload); + + // Both users should have yield proportional to their principal + // USER1 has ~500k principal, USER2 has ~500k principal (from transfer) + // So each should get approximately 500k yield + uint256 user1NewYield = usdn.yield(USER1); + uint256 user2NewYield = usdn.yield(USER2); + + // Due to rounding, yields might not be exactly 500k each + assertApproxEqAbs(user1NewYield, 75e10, 2, "USER1 should have ~750k new yield"); + assertApproxEqAbs(user2NewYield, 25e10, 2, "USER1 should have ~250k new yield"); + + // USER1 claims their yield + vm.prank(USER1); + usdn.claim(); + assertEq(usdn.balanceOf(USER1), 225e10, "USER1 should have 2.25M balance after claiming yield"); + + // USER2 claims their yield + vm.prank(USER2); + usdn.claim(); + assertEq(usdn.balanceOf(USER2), 75e10, "USER2 should have 750k balance after claiming yield"); + } + + function test_indexUpdateWithZeroTotalPrincipal() public { + // Edge case: yield accrual when totalPrincipal is 0 (no deposits) + // The contract should handle this gracefully without reverting + + assertEq(usdn.totalSupply(), 0, "Total supply should be 0 initially"); + assertEq(usdn.totalPrincipal(), 0, "Total principal should be 0 initially"); + assertEq(usdn.index(), 1e12, "Index should be 1.0 initially"); + + // Try to accrue yield when no principal exists + bytes memory yieldPayload = abi.encodeWithSignature( + "process(bytes,bytes)", + 0x0, + hex"03000000014e4f424c726f757465725f6170700000000000000000000000000001000000000000000000000001000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000000000000000000000000000000000e8d4a51000" + ); + (bool success,) = MAILBOX.call(yieldPayload); + assertTrue(success, "Yield accrual should succeed even with zero principal"); + + // Index should remain unchanged when totalPrincipal is 0 + assertEq(usdn.index(), 1e12, "Index should remain 1.0 when no principal exists"); + assertEq(usdn.totalSupply(), 1e12, "Supply should increase by yield amount"); + assertEq(usdn.totalPrincipal(), 0, "Total principal should still be 0"); + } + + function test_indexUpdateAfterBurn() public { + // Setup: Mint 1M to USER1 + bytes memory mintPayload = abi.encodeWithSignature( + "process(bytes,bytes)", + 0x0, + hex"03000000004e4f424c726f757465725f6170700000000000000000000000000001000000000000000000000001000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000000000e8d4a51000" + ); + (bool success,) = MAILBOX.call(mintPayload); + assertTrue(success, "Initial mint should succeed"); + + // Accrue 100% yield + bytes memory yieldPayload1 = abi.encodeWithSignature( + "process(bytes,bytes)", + 0x0, + hex"03000000014e4f424c726f757465725f6170700000000000000000000000000001000000000000000000000001000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000000000000000000000000000000000e8d4a51000" + ); + (success,) = MAILBOX.call(yieldPayload1); + assertTrue(success, "Yield accrual should succeed"); + assertEq(usdn.index(), 2e12, "Index should be 2.0 after 100% yield"); + + // Burn entire pre yield claimed balance of USER1 + vm.prank(USER1); + usdn.transferRemote{value: 1 ether}( + 1313817164, // destination domain (you used this in setUp) + bytes32(uint256(uint160(USER2))), + 1e12 // amount to transfer (1M USDN) + ); + + uint256 _balanceUSER1 = usdn.balanceOf(USER1); + uint256 _principalUSER1 = usdn.principalOf(USER1); + + assertEq(_balanceUSER1, 0, "USER1 balance should be 0 after transfer"); + assertEq(_principalUSER1, 5e11, "USER1 principal should be halved after burn"); + + assertEq(usdn.totalPrincipal(), 5e11, "Total principal should be 500k after burn"); + assertEq(usdn.totalSupply(), 1e12, "Contract should still hold unclaimed yield"); + + // Claim entire yield with remaining USER1 principal + vm.prank(USER1); + usdn.claim(); + + _balanceUSER1 = usdn.balanceOf(USER1); + _principalUSER1 = usdn.principalOf(USER1); + + assertEq(_balanceUSER1, 1e12, "USER1 should have all yield as balance after claiming"); + assertEq(_principalUSER1, 5e11, "USER1 principal should remain the same"); + + // Realize 100% yield again, 1m tokens + bytes memory yieldPayload2 = abi.encodeWithSignature( + "process(bytes,bytes)", + 0x0, + hex"03000000024e4f424c726f757465725f6170700000000000000000000000000001000000000000000000000001000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a000000000000000000000000000000000000000000000000000000e8d4a51000" + ); + (success,) = MAILBOX.call(yieldPayload2); + assertTrue(success, "Second yield accrual should succeed"); + + _balanceUSER1 = usdn.balanceOf(USER1); + _principalUSER1 = usdn.principalOf(USER1); + + assertEq(usdn.index(), 4e12, "index should be doubled again"); + } } diff --git a/contracts/utils/UIntMath.sol b/contracts/utils/UIntMath.sol index 3994d015..4d1df885 100644 --- a/contracts/utils/UIntMath.sol +++ b/contracts/utils/UIntMath.sol @@ -36,4 +36,14 @@ library UIntMath { if (n > type(uint128).max) revert InvalidUInt128(); return uint128(n); } + + /** + * @notice Takes the min between two uint256 values. + * @param a First value + * @param b Second value + * @return The lesser of the two + */ + function min256(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } }