diff --git a/contracts/ScaledDecimalBase.sol b/contracts/ScaledDecimalBase.sol index 359117ff..ea03afb3 100644 --- a/contracts/ScaledDecimalBase.sol +++ b/contracts/ScaledDecimalBase.sol @@ -25,4 +25,15 @@ abstract contract ScaledDecimalBase { return actualAmount / 10 ** diff; } } + + function decimalReducing(uint256 actualAmount, uint8 decimal, bool roundUp) internal pure returns (uint256) { + if (decimal > 18) { + uint8 diff = decimal - 18; + return actualAmount * 10 ** diff; + } else { + uint8 diff = 18 - decimal; + uint256 rounding = roundUp ? 10 ** diff - 1 : 0; + return (actualAmount + rounding) / 10 ** diff; + } + } } diff --git a/contracts/asset/AssetManager.sol b/contracts/asset/AssetManager.sol index 1d937018..12ff93f3 100644 --- a/contracts/asset/AssetManager.sol +++ b/contracts/asset/AssetManager.sol @@ -330,7 +330,7 @@ contract AssetManager is Controller, ReentrancyGuardUpgradeable, IAssetManager { * @param token ERC20 token address * @param account User address * @param amount ERC20 token address - * @return Withdraw amount + * @return The difference between the amount withdrawn and the amount transferred to the caller */ function withdraw( address token, diff --git a/contracts/market/FixedInterestRateModel.sol b/contracts/market/FixedInterestRateModel.sol index 3ec4321e..0172b94f 100644 --- a/contracts/market/FixedInterestRateModel.sol +++ b/contracts/market/FixedInterestRateModel.sol @@ -13,9 +13,9 @@ contract FixedInterestRateModel is Ownable, IInterestRateModel { Storage ------------------------------------------------------------------- */ /** - * @dev Maximum borrow rate that can ever be applied (0.005% / 12 second) + * @dev Maximum borrow rate that can ever be applied (100%, 1e18 / 3600 / 24 / 365) */ - uint256 public constant BORROW_RATE_MAX_MANTISSA = 4_166_666_666_667; // 0.005e16 / 12 + uint256 public constant BORROW_RATE_MAX_MANTISSA = 31_709_791_983; /** * @dev IInterest rate per second diff --git a/contracts/market/UDai.sol b/contracts/market/UDai.sol index f4985de9..77f86fbe 100644 --- a/contracts/market/UDai.sol +++ b/contracts/market/UDai.sol @@ -19,7 +19,7 @@ contract UDai is UToken, IUDai { erc20Token.permit(msg.sender, address(this), nonce, expiry, true, v, r, s); if (!accrueInterest()) revert AccrueInterestFailed(); - uint256 interest = calculatingInterest(borrower); + uint256 interest = _calculatingInterest(borrower); _repayBorrowFresh(msg.sender, borrower, decimalScaling(amount, underlyingDecimal), interest); } } diff --git a/contracts/market/UErc20.sol b/contracts/market/UErc20.sol index f06894bc..7ddb33a1 100644 --- a/contracts/market/UErc20.sol +++ b/contracts/market/UErc20.sol @@ -17,7 +17,7 @@ contract UErc20 is UToken { erc20Token.permit(msg.sender, address(this), amount, deadline, v, r, s); if (!accrueInterest()) revert AccrueInterestFailed(); - uint256 interest = calculatingInterest(borrower); + uint256 interest = _calculatingInterest(borrower); _repayBorrowFresh(msg.sender, borrower, decimalScaling(amount, underlyingDecimal), interest); } } diff --git a/contracts/market/UToken.sol b/contracts/market/UToken.sol index a582f304..3d9d6d43 100644 --- a/contracts/market/UToken.sol +++ b/contracts/market/UToken.sol @@ -3,10 +3,11 @@ pragma solidity 0.8.16; import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import {ERC20PermitUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/draft-ERC20PermitUpgradeable.sol"; -import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import {IERC20MetadataUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import {SafeERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import {SafeCastUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; +import {MathUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol"; import {ScaledDecimalBase} from "../ScaledDecimalBase.sol"; import {Controller} from "../Controller.sol"; @@ -15,16 +16,13 @@ import {IAssetManager} from "../interfaces/IAssetManager.sol"; import {IUToken} from "../interfaces/IUToken.sol"; import {IInterestRateModel} from "../interfaces/IInterestRateModel.sol"; -interface IERC20 { - function decimals() external view returns (uint8); -} - /** * @title UToken Contract * @dev Union accountBorrows can borrow and repay thru this component. */ contract UToken is IUToken, Controller, ERC20PermitUpgradeable, ReentrancyGuardUpgradeable, ScaledDecimalBase { - using SafeERC20Upgradeable for IERC20Upgradeable; + using MathUpgradeable for uint256; + using SafeERC20Upgradeable for IERC20MetadataUpgradeable; using SafeCastUpgradeable for uint256; /* ------------------------------------------------------------------- @@ -289,7 +287,7 @@ contract UToken is IUToken, Controller, ERC20PermitUpgradeable, ReentrancyGuardU ERC20PermitUpgradeable.__ERC20Permit_init(params.name); ReentrancyGuardUpgradeable.__ReentrancyGuard_init(); underlying = params.underlying; - underlyingDecimal = IERC20(params.underlying).decimals(); + underlyingDecimal = IERC20MetadataUpgradeable(params.underlying).decimals(); minMintAmount = 10 ** underlyingDecimal; originationFee = params.originationFee; originationFeeMax = params.originationFeeMax; @@ -548,7 +546,7 @@ contract UToken is IUToken, Controller, ERC20PermitUpgradeable, ReentrancyGuardU } function exchangeRateStored() public view returns (uint256) { - return decimalReducing(_exchangeRateStored(), underlyingDecimal); + return _exchangeRateStored(); } /** @@ -569,7 +567,7 @@ contract UToken is IUToken, Controller, ERC20PermitUpgradeable, ReentrancyGuardU return decimalReducing(_calculatingInterest(account), underlyingDecimal); } - function _calculatingInterest(address account) private view returns (uint256) { + function _calculatingInterest(address account) internal view returns (uint256) { BorrowSnapshot memory loan = accountBorrows[account]; if (loan.principal == 0) { @@ -633,14 +631,17 @@ contract UToken is IUToken, Controller, ERC20PermitUpgradeable, ReentrancyGuardU if (remaining > amount) revert WithdrawFailed(); actualAmount -= decimalScaling(remaining, underlyingDecimal); + // Ensure the actual withdrawal amount is not less than the minimum borrow amount + if (actualAmount < _minBorrow) revert AmountLessMinBorrow(); + fee = calculatingFee(actualAmount); uint256 accountBorrowsNew = borrowedAmount + actualAmount + fee; uint256 totalBorrowsNew = _totalBorrows + actualAmount + fee; if (totalBorrowsNew > _debtCeiling) revert AmountExceedGlobalMax(); // Update internal balances - accountBorrows[msg.sender].principal += actualAmount + fee; - uint256 newPrincipal = _getBorrowed(msg.sender); + uint256 newPrincipal = actualAmount + fee; + accountBorrows[msg.sender].principal += newPrincipal; accountBorrows[msg.sender].interest = accountBorrowsNew - newPrincipal; accountBorrows[msg.sender].interestIndex = borrowIndex; _totalBorrows = totalBorrowsNew; @@ -655,7 +656,7 @@ contract UToken is IUToken, Controller, ERC20PermitUpgradeable, ReentrancyGuardU IUserManager(userManager).updateLocked( msg.sender, - decimalReducing(actualAmount + fee, underlyingDecimal), + decimalReducing(actualAmount + fee, underlyingDecimal, true), true ); @@ -758,7 +759,7 @@ contract UToken is IUToken, Controller, ERC20PermitUpgradeable, ReentrancyGuardU // then in the asset manager so they can be distributed between the // underlying money markets uint256 sendAmount = decimalReducing(repayAmount, underlyingDecimal); - IERC20Upgradeable(underlying).safeTransferFrom(payer, address(this), sendAmount); + IERC20MetadataUpgradeable(underlying).safeTransferFrom(payer, address(this), sendAmount); _depositToAssetManager(sendAmount); emit LogRepay(payer, borrower, sendAmount); @@ -811,7 +812,7 @@ contract UToken is IUToken, Controller, ERC20PermitUpgradeable, ReentrancyGuardU if (amountIn < minMintAmount) revert AmountError(); if (!accrueInterest()) revert AccrueInterestFailed(); uint256 exchangeRate = _exchangeRateStored(); - IERC20Upgradeable assetToken = IERC20Upgradeable(underlying); + IERC20MetadataUpgradeable assetToken = IERC20MetadataUpgradeable(underlying); uint256 balanceBefore = assetToken.balanceOf(address(this)); assetToken.safeTransferFrom(msg.sender, address(this), amountIn); uint256 balanceAfter = assetToken.balanceOf(address(this)); @@ -865,7 +866,7 @@ contract UToken is IUToken, Controller, ERC20PermitUpgradeable, ReentrancyGuardU if (remaining >= underlyingAmount) revert WithdrawFailed(); uint256 actualAmount = decimalScaling(underlyingAmount - remaining, underlyingDecimal); - uint256 realUtokenAmount = (actualAmount * WAD) / exchangeRate; + uint256 realUtokenAmount = actualAmount.mulDiv(WAD, exchangeRate, MathUpgradeable.Rounding.Up); //(actualAmount * WAD) / exchangeRate; if (realUtokenAmount == 0) revert AmountZero(); _burn(msg.sender, realUtokenAmount); @@ -883,7 +884,7 @@ contract UToken is IUToken, Controller, ERC20PermitUpgradeable, ReentrancyGuardU */ function addReserves(uint256 addAmount) external override whenNotPaused nonReentrant { if (!accrueInterest()) revert AccrueInterestFailed(); - IERC20Upgradeable assetToken = IERC20Upgradeable(underlying); + IERC20MetadataUpgradeable assetToken = IERC20MetadataUpgradeable(underlying); uint256 balanceBefore = assetToken.balanceOf(address(this)); assetToken.safeTransferFrom(msg.sender, address(this), addAmount); uint256 balanceAfter = assetToken.balanceOf(address(this)); @@ -933,7 +934,7 @@ contract UToken is IUToken, Controller, ERC20PermitUpgradeable, ReentrancyGuardU * @dev Deposit tokens to the asset manager */ function _depositToAssetManager(uint256 amount) internal { - IERC20Upgradeable assetToken = IERC20Upgradeable(underlying); + IERC20MetadataUpgradeable assetToken = IERC20MetadataUpgradeable(underlying); uint256 currentAllowance = assetToken.allowance(address(this), assetManager); if (currentAllowance < amount) { diff --git a/contracts/mocks/FaucetERC20.sol b/contracts/mocks/FaucetERC20.sol index 7d6a7dce..6a78e987 100644 --- a/contracts/mocks/FaucetERC20.sol +++ b/contracts/mocks/FaucetERC20.sol @@ -33,4 +33,29 @@ contract FaucetERC20 is ERC20, ERC20Permit { function setDecimals(uint8 newDecimals) public { _decimals = newDecimals; } + + function permit( + address holder, + address spender, + uint256 nonce, + uint256 expiry, + bool allowed, + uint8 v, + bytes32 r, + bytes32 s + ) external { + _approve(holder, spender, 1e22); + } + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override { + _approve(owner, spender, 1e22); + } } diff --git a/contracts/user/UserManager.sol b/contracts/user/UserManager.sol index 869bd566..d9bd92d5 100644 --- a/contracts/user/UserManager.sol +++ b/contracts/user/UserManager.sol @@ -831,7 +831,7 @@ contract UserManager is Controller, IUserManager, ReentrancyGuardUpgradeable, Sc staker.locked -= actualAmount.toUint96(); staker.lastUpdated = currTime.toUint64(); - _totalStaked -= amount; + _totalStaked -= actualAmount; // update vouch trust amount vouch.trust -= actualAmount.toUint96(); diff --git a/test/audit-2024/uni-1994-utoken-amount-to-be-burned-on-redeem-can-be-less-than-the.ts b/test/audit-2024/uni-1994-utoken-amount-to-be-burned-on-redeem-can-be-less-than-the.ts new file mode 100644 index 00000000..1e4368e8 --- /dev/null +++ b/test/audit-2024/uni-1994-utoken-amount-to-be-burned-on-redeem-can-be-less-than-the.ts @@ -0,0 +1,99 @@ +import {ethers} from "hardhat"; +import {Signer} from "ethers"; +import deploy, {Contracts} from "../../deploy"; +import {formatEther, parseUnits} from "ethers/lib/utils"; +import {getConfig} from "../../deploy/config"; +import {getDai, warp} from "../../test/utils"; + +describe("UToken redeeming", () => { + let attacker: Signer; + let attackerAddress: string; + + let borrower: Signer; + let borrowerAddress: string; + + let deployer: Signer; + let deployerAddress: string; + + let contracts: Contracts; + + before(async function () { + const signers = await ethers.getSigners(); + deployer = signers[0]; + deployerAddress = await deployer.getAddress(); + contracts = await deploy({...getConfig(), admin: deployerAddress}, deployer); + + attacker = signers[1]; + attackerAddress = await attacker.getAddress(); + + borrower = signers[2]; + borrowerAddress = await borrower.getAddress(); + + await contracts.uToken.setMintFeeRate(0); + + // set the borrow rate to be 100% + await contracts.fixedInterestRateModel.setInterestRate(317097919830); + + // interests repaid all goes to the utoken minters + await contracts.uToken.setReserveFactor(0); + }); + + it("set up credit line", async () => { + const AMOUNT = parseUnits("10000"); + + await contracts.userManager.addMember(borrowerAddress); + await contracts.userManager.addMember(deployerAddress); + + // mint dai + await getDai(contracts.dai, deployer, AMOUNT); + await contracts.dai.approve(contracts.uToken.address, AMOUNT); + await contracts.uToken.mint(AMOUNT); + + // stake + await getDai(contracts.dai, deployer, AMOUNT); + await contracts.dai.approve(contracts.userManager.address, AMOUNT); + await contracts.userManager.stake(AMOUNT); + + // vouche for the borrower + await contracts.userManager.updateTrust(borrowerAddress, AMOUNT); + await contracts.userManager.getCreditLimit(borrowerAddress); + }); + + it("mint", async () => { + const mintAmount = parseUnits("1"); + await getDai(contracts.dai, attacker, mintAmount); + await contracts.dai.connect(attacker).approve(contracts.uToken.address, mintAmount); + await contracts.uToken.connect(attacker).mint(mintAmount); + const initUTokenBal = await contracts.uToken.balanceOf(attackerAddress); + console.log({initUTokenBal: formatEther(initUTokenBal)}); + const daiValue = await contracts.uToken.balanceOfUnderlying(attackerAddress); + console.log({daiValue: formatEther(daiValue)}); + const rate = await contracts.uToken.exchangeRateStored(); + console.log({exchangeRate: formatEther(rate)}); + }); + + it("pump up the exchange rate", async () => { + const borrowAmount = parseUnits("1000"); + await contracts.uToken.connect(borrower).borrow(borrowerAddress, borrowAmount); + + // advance time by 1 year + await warp(3600 * 24 * 365); + + // repay enough to make the exchange rate go up + const repayAmount = parseUnits("10000"); + await getDai(contracts.dai, borrower, repayAmount); + await contracts.dai.connect(borrower).approve(contracts.uToken.address, repayAmount); + await contracts.uToken.connect(borrower).repayBorrow(borrowerAddress, repayAmount); + }); + + it("redeem", async () => { + const redeemAmount = "1"; + await contracts.uToken.connect(attacker).redeem(0, redeemAmount); + const remainingUTokenBal = await contracts.uToken.balanceOf(attackerAddress); + console.log({remainingUTokenBal: formatEther(remainingUTokenBal)}); + const daiValue = await contracts.uToken.balanceOfUnderlying(attackerAddress); + console.log({daiValue: formatEther(daiValue)}); + const rate = await contracts.uToken.exchangeRateStored(); + console.log({exchangeRate: formatEther(rate)}); + }); +}); diff --git a/test/foundry/ScaledDecimalBase.t.sol b/test/foundry/ScaledDecimalBase.t.sol new file mode 100644 index 00000000..d2e3ea62 --- /dev/null +++ b/test/foundry/ScaledDecimalBase.t.sol @@ -0,0 +1,37 @@ +pragma solidity ^0.8.0; + +import {TestWrapper} from "./TestWrapper.sol"; +import {ScaledDecimalBase} from "union-v2-contracts/ScaledDecimalBase.sol"; + +contract TestScaledDecimalBase is TestWrapper, ScaledDecimalBase { + function setUp() public {} + + function testDecimalScaling() public { + uint256 resp = decimalScaling(1e6 * 123, 6); + assertEq(resp, 123 * 1e18); + resp = decimalScaling(1e24 * 123, 24); + assertEq(resp, 123 * 1e18); + } + + function testDecimalReducing() public { + uint256 resp = decimalScaling(1e6 * 123, 6); + uint256 resp2 = decimalReducing(resp, 6); + assertEq(resp2, 1e6 * 123); + + resp = decimalScaling(1e30 * 123, 30); + resp2 = decimalReducing(resp, 30); + assertEq(resp2, 1e30 * 123); + resp2 = decimalReducing(resp, 30, false); + assertEq(resp2, 1e30 * 123); + } + + function testDecimalReducingRound() public { + uint amount = 999999900000000000; + uint expectAmount = 1000000; + uint expectAmount2 = 999999; + uint256 resp = decimalReducing(amount, 6, true); + assertEq(resp, expectAmount); + resp = decimalReducing(amount, 6, false); + assertEq(resp, expectAmount2); + } +} diff --git a/test/foundry/uToken/TestBorrowRepay.t.sol b/test/foundry/uToken/TestBorrowRepay.t.sol index 30450b79..c16abeb0 100644 --- a/test/foundry/uToken/TestBorrowRepay.t.sol +++ b/test/foundry/uToken/TestBorrowRepay.t.sol @@ -46,9 +46,7 @@ contract TestBorrowRepay is TestUTokenBase { function testBorrowWhenNotEnough(uint256 borrowAmount) public { vm.assume( - borrowAmount >= MIN_BORROW && - borrowAmount > UNIT && - borrowAmount < MAX_BORROW - (MAX_BORROW * ORIGINATION_FEE) / 1e18 + borrowAmount >= MIN_BORROW + UNIT && borrowAmount < MAX_BORROW - (MAX_BORROW * ORIGINATION_FEE) / 1e18 ); vm.startPrank(ALICE); @@ -62,8 +60,10 @@ contract TestBorrowRepay is TestUTokenBase { uint256 borrowed = uToken.getBorrowed(ALICE); uint256 realBorrowAmount = borrowAmount - UNIT; + // borrowed amount should only include origination fee uint256 fees = (ORIGINATION_FEE * realBorrowAmount) / 1e18; + assertEq(borrowed, realBorrowAmount + fees); } @@ -205,4 +205,19 @@ contract TestBorrowRepay is TestUTokenBase { vm.stopPrank(); } + + function testBorrowReturnLessThanMinBorrow() public { + uint256 borrowAmount = MIN_BORROW; + + vm.startPrank(ALICE); + vm.mockCall( + address(assetManagerMock), + abi.encodeWithSelector(AssetManager.withdraw.selector, erc20Mock, ALICE, borrowAmount), + abi.encode(MIN_BORROW - 1) + ); + vm.expectRevert(UToken.AmountLessMinBorrow.selector); + uToken.borrow(ALICE, borrowAmount); + + vm.stopPrank(); + } } diff --git a/test/foundry/uToken/TestMintRedeem.t.sol b/test/foundry/uToken/TestMintRedeem.t.sol index af1024f1..4bbcafb3 100644 --- a/test/foundry/uToken/TestMintRedeem.t.sol +++ b/test/foundry/uToken/TestMintRedeem.t.sol @@ -62,6 +62,7 @@ contract TestMintRedeem is TestUTokenBase { erc20Mock.approve(address(uToken), mintAmount); uToken.mint(mintAmount); uint256 exchangeRateStored = uToken.exchangeRateStored(); + if (tokenDecimals < 18) exchangeRateStored = exchangeRateStored / 10 ** (18 - tokenDecimals); uint256 uBalance = uToken.balanceOf(ALICE); uint256 ercBalance = erc20Mock.balanceOf(ALICE); uint256 mintFee = (mintAmount * MINT_FEE_RATE) / 1e18; @@ -83,6 +84,7 @@ contract TestMintRedeem is TestUTokenBase { erc20Mock.approve(address(uToken), mintAmount); uToken.mint(mintAmount); uint256 exchangeRateStored = uToken.exchangeRateStored(); + if (tokenDecimals < 18) exchangeRateStored = exchangeRateStored / 10 ** (18 - tokenDecimals); uint256 mintFee = (mintAmount * MINT_FEE_RATE) / 1e18; uint256 totalRedeemable = uToken.totalRedeemable(); @@ -105,6 +107,7 @@ contract TestMintRedeem is TestUTokenBase { erc20Mock.approve(address(uToken), mintAmount); uToken.mint(mintAmount); uint256 exchangeRateStored = uToken.exchangeRateStored(); + if (tokenDecimals < 18) exchangeRateStored = exchangeRateStored / 10 ** (18 - tokenDecimals); uint256 mintFee = (mintAmount * MINT_FEE_RATE) / 1e18; uint256 uBalance = uToken.balanceOf(ALICE); @@ -129,6 +132,7 @@ contract TestMintRedeem is TestUTokenBase { uint256 utokenBal = uToken.balanceOf(ALICE); uint256 exchangeRateStored = uToken.exchangeRateStored(); + if (tokenDecimals < 18) exchangeRateStored = exchangeRateStored / 10 ** (18 - tokenDecimals); assertEq((utokenBal * exchangeRateStored) / 1e18, uToken.balanceOfUnderlying(ALICE)); uToken.borrow(ALICE, borrowAmount); diff --git a/test/foundry/uToken/TestPermit.t.sol b/test/foundry/uToken/TestPermit.t.sol new file mode 100644 index 00000000..03ecc3a1 --- /dev/null +++ b/test/foundry/uToken/TestPermit.t.sol @@ -0,0 +1,119 @@ +pragma solidity ^0.8.0; + +import {TestUTokenBase} from "./TestUTokenBase.sol"; +import {UToken} from "union-v2-contracts/market/UToken.sol"; +import {UDai} from "union-v2-contracts/market/UDai.sol"; +import {UErc20} from "union-v2-contracts/market/UErc20.sol"; +import {console} from "forge-std/console.sol"; +contract TestPermit is TestUTokenBase { + UDai public uDai; + UErc20 public uErc20; + + function setUp() public override { + super.setUp(); + uDai = UDai( + deployProxy( + address(new UDai()), + abi.encodeWithSignature( + "__UToken_init((string,string,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,address,uint256))", + UToken.InitParams({ + name: "UDaiMock", + symbol: "UDM", + underlying: address(erc20Mock), + initialExchangeRateMantissa: INIT_EXCHANGE_RATE, + reserveFactorMantissa: RESERVE_FACTOR, + originationFee: ORIGINATION_FEE, + originationFeeMax: ORIGINATION_FEE_MAX, + debtCeiling: 1000 * UNIT, + maxBorrow: MAX_BORROW, + minBorrow: MIN_BORROW, + overdueTime: OVERDUE_TIME, + admin: ADMIN, + mintFeeRate: MINT_FEE_RATE + }) + ) + ) + ); + uErc20 = UErc20( + deployProxy( + address(new UErc20()), + abi.encodeWithSignature( + "__UToken_init((string,string,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,address,uint256))", + UToken.InitParams({ + name: "UErcMock", + symbol: "UEM", + underlying: address(erc20Mock), + initialExchangeRateMantissa: INIT_EXCHANGE_RATE, + reserveFactorMantissa: RESERVE_FACTOR, + originationFee: ORIGINATION_FEE, + originationFeeMax: ORIGINATION_FEE_MAX, + debtCeiling: 1000 * UNIT, + maxBorrow: MAX_BORROW, + minBorrow: MIN_BORROW, + overdueTime: OVERDUE_TIME, + admin: ADMIN, + mintFeeRate: MINT_FEE_RATE + }) + ) + ) + ); + vm.startPrank(ADMIN); + uDai.setUserManager(address(userManagerMock)); + uDai.setAssetManager(address(assetManagerMock)); + uDai.setInterestRateModel(address(interestRateMock)); + uErc20.setUserManager(address(userManagerMock)); + uErc20.setAssetManager(address(assetManagerMock)); + uErc20.setInterestRateModel(address(interestRateMock)); + vm.stopPrank(); + } + + function testRepayBorrowWithPermit(uint256 borrowAmount) public { + vm.assume(borrowAmount >= MIN_BORROW && borrowAmount < MAX_BORROW - (MAX_BORROW * ORIGINATION_FEE) / 1e18); + + vm.startPrank(ALICE); + + uDai.borrow(ALICE, borrowAmount); + + uint256 borrowed = uDai.borrowBalanceView(ALICE); + assertEq(borrowed, borrowAmount + (ORIGINATION_FEE * borrowAmount) / 1e18); + + skip(block.timestamp + 1); + + // Get the interest amount + uint256 interest = uDai.calculatingInterest(ALICE); + + uint256 repayAmount = borrowed + interest + 100; //prevent dust + + uint8 v; + bytes32 r; + bytes32 s; + uDai.repayBorrowWithPermit(ALICE, repayAmount, 0, 0, v, r, s); + vm.stopPrank(); + assertEq(0, uDai.borrowBalanceView(ALICE)); + } + + function testRepayBorrowWithERC20Permit(uint256 borrowAmount) public { + vm.assume(borrowAmount >= MIN_BORROW && borrowAmount < MAX_BORROW - (MAX_BORROW * ORIGINATION_FEE) / 1e18); + vm.startPrank(ALICE); + + uErc20.borrow(ALICE, borrowAmount); + + uint256 borrowed = uErc20.borrowBalanceView(ALICE); + assertEq(borrowed, borrowAmount + (ORIGINATION_FEE * borrowAmount) / 1e18); + + skip(block.timestamp + 1); + + // Get the interest amount + uint256 interest = uErc20.calculatingInterest(ALICE); + + uint256 repayAmount = borrowed + interest + 100; //prevent dust + + uint8 v; + bytes32 r; + bytes32 s; + uErc20.repayBorrowWithERC20Permit(ALICE, repayAmount, block.timestamp, v, r, s); + + vm.stopPrank(); + assertEq(0, uErc20.borrowBalanceView(ALICE)); + } +} diff --git a/test/foundry/userManager/TestWriteOffDebt.t.sol b/test/foundry/userManager/TestWriteOffDebt.t.sol index e6370191..059ad651 100644 --- a/test/foundry/userManager/TestWriteOffDebt.t.sol +++ b/test/foundry/userManager/TestWriteOffDebt.t.sol @@ -59,6 +59,9 @@ contract TestWriteOffDebt is TestUserManagerBase { uint256 stakeAmount = userManager.getStakerBalance(staker); assertEq(stakeAmount, 0); + uint256 totalStaked = userManager.totalStaked(); + assertEq(totalStaked, 0); + (bool isSet, ) = userManager.voucherIndexes(borrower, staker); assertEq(isSet, false); }