Skip to content

Commit

Permalink
Merge branch 'feature/uni-1825-deploy-to-base-sepolia-testnet' into f…
Browse files Browse the repository at this point in the history
…inding/uni-1989-wrong-calculation-of-accure-reward-in-comptrollersol

# Conflicts:
#	slither.db.json
  • Loading branch information
maxweng committed Aug 31, 2024
2 parents 7c39016 + 93934fa commit d3197fc
Show file tree
Hide file tree
Showing 14 changed files with 340 additions and 26 deletions.
11 changes: 11 additions & 0 deletions contracts/ScaledDecimalBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
2 changes: 1 addition & 1 deletion contracts/asset/AssetManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions contracts/market/FixedInterestRateModel.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion contracts/market/UDai.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
2 changes: 1 addition & 1 deletion contracts/market/UErc20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
35 changes: 18 additions & 17 deletions contracts/market/UToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;

/* -------------------------------------------------------------------
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -548,7 +546,7 @@ contract UToken is IUToken, Controller, ERC20PermitUpgradeable, ReentrancyGuardU
}

function exchangeRateStored() public view returns (uint256) {
return decimalReducing(_exchangeRateStored(), underlyingDecimal);
return _exchangeRateStored();
}

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

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

Expand All @@ -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));
Expand Down Expand Up @@ -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) {
Expand Down
25 changes: 25 additions & 0 deletions contracts/mocks/FaucetERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
2 changes: 1 addition & 1 deletion contracts/user/UserManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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)});
});
});
37 changes: 37 additions & 0 deletions test/foundry/ScaledDecimalBase.t.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit d3197fc

Please sign in to comment.