diff --git a/contracts/MyContracts.sol b/contracts/MyContracts.sol index 650d45a..4568dc9 100644 --- a/contracts/MyContracts.sol +++ b/contracts/MyContracts.sol @@ -1,127 +1,157 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.6; +// Using the recommended latest stable Solidity version for best security and gas optimization. +pragma solidity ^0.8.20; +// Custom Errors for gas efficiency (replaces string reverts) +error MintFailed(uint256 error); +error EnterMarketsFailed(uint256 errorCode); +error GetLiquidityFailed(uint256 errorCode); +error InsufficientLiquidity(); +error BorrowFailed(uint256 errorCode); +error RepayFailed(uint256 errorCode); +error InvalidOption(string reason); +error AddressNotContract(address _address); -interface Erc20 { - function approve(address, uint256) external returns (bool); +// --- Interface Definitions --- - function transfer(address, uint256) external returns (bool); +interface Erc20 { + function approve(address spender, uint256 amount) external returns (bool); + function transfer(address recipient, uint256 amount) external returns (bool); + // Added common balance/allowance views for completeness + function balanceOf(address account) external view returns (uint256); } interface CErc20 { - function mint(uint256) external returns (uint256); - - function borrow(uint256) external returns (uint256); - + function mint(uint256 mintAmount) external returns (uint256); + function borrow(uint256 borrowAmount) external returns (uint256); function borrowRatePerBlock() external view returns (uint256); - - function borrowBalanceCurrent(address) external returns (uint256); - - function repayBorrow(uint256) external returns (uint256); + function borrowBalanceCurrent(address account) external returns (uint256); + function repayBorrow(uint256 repayAmount) external returns (uint256); } interface CEth { function mint() external payable; - - function borrow(uint256) external returns (uint256); - - function repayBorrow() external payable; - - function borrowBalanceCurrent(address) external returns (uint256); + function borrow(uint256 borrowAmount) external returns (uint256); + function repayBorrow() external payable; // Repay ETH by sending ETH in `value` + function borrowBalanceCurrent(address account) external returns (uint256); } interface Comptroller { - function markets(address) external returns (bool, uint256); - - function enterMarkets(address[] calldata) - external - returns (uint256[] memory); - - function getAccountLiquidity(address) - external - view - returns (uint256, uint256, uint256); + // isListed, collateralFactorMantissa + function markets(address cToken) external view returns (bool, uint256); + function enterMarkets(address[] calldata cTokens) external returns (uint256[] memory); + // error, liquidity, shortfall + function getAccountLiquidity(address account) external view returns (uint256, uint256, uint256); } interface PriceFeed { - function getUnderlyingPrice(address cToken) external view returns (uint); + // Returns the underlying price in USD scaled by 1e18 + function getUnderlyingPrice(address cToken) external view returns (uint256); } -contract MyContract { - event MyLog(string, uint256); - - // Seed the contract with a supported underyling asset before running this +/** + * @title CompoundBorrowingDemo + * @notice Demonstrates basic supply, enter market, borrow, and repay actions on Compound-like protocols. + */ +contract CompoundBorrowingDemo { + event MyLog(string message, uint256 value); + + // Immutable addresses for core protocol components for safety and gas efficiency + address immutable public cEthAddress; + address immutable public comptrollerAddress; + address immutable public priceFeedAddress; + + constructor(address _cEthAddress, address _comptrollerAddress, address _priceFeedAddress) { + cEthAddress = _cEthAddress; + comptrollerAddress = _comptrollerAddress; + priceFeedAddress = _priceFeedAddress; + } + + /** + * @notice Supplies ETH as collateral and then borrows a specified ERC20 token. + * @dev Assumes the caller has already seeded this contract with the underlying ERC20 token for repayment. + * @param _cTokenAddress Address of the cToken to borrow (e.g., cDAI, cUSDC). + * @param _underlyingDecimals Decimals of the underlying token (e.g., 18 for DAI, 6 for USDC). + * @return Current outstanding borrow balance of the underlying token. + */ function borrowErc20Example( - address payable _cEtherAddress, - address _comptrollerAddress, - address _priceFeedAddress, address _cTokenAddress, - uint _underlyingDecimals + uint256 _underlyingDecimals ) public payable returns (uint256) { - CEth cEth = CEth(_cEtherAddress); - Comptroller comptroller = Comptroller(_comptrollerAddress); - PriceFeed priceFeed = PriceFeed(_priceFeedAddress); + CEth cEth = CEth(cEthAddress); + Comptroller comptroller = Comptroller(comptrollerAddress); + PriceFeed priceFeed = PriceFeed(priceFeedAddress); CErc20 cToken = CErc20(_cTokenAddress); - // Supply ETH as collateral, get cETH in return - cEth.mint{ value: msg.value, gas: 250000 }(); + // 1. Supply ETH as collateral, receiving cETH in return. + // REMOVED fixed gas limit (gas: 250000) for security. + cEth.mint{ value: msg.value }(); - // Enter the ETH market so you can borrow another type of asset + // 2. Enter the cETH market to enable borrowing against it. address[] memory cTokens = new address[](1); - cTokens[0] = _cEtherAddress; + cTokens[0] = cEthAddress; uint256[] memory errors = comptroller.enterMarkets(cTokens); + if (errors[0] != 0) { - revert("Comptroller.enterMarkets failed."); + revert EnterMarketsFailed(errors[0]); // Use Custom Error } - // Get my account's total liquidity value in Compound + // 3. Get my account's current liquidity status. (uint256 error, uint256 liquidity, uint256 shortfall) = comptroller .getAccountLiquidity(address(this)); + if (error != 0) { - revert("Comptroller.getAccountLiquidity failed."); + revert GetLiquidityFailed(error); // Use Custom Error } - require(shortfall == 0, "account underwater"); - require(liquidity > 0, "account has excess collateral"); - - // Get the collateral factor for our collateral - // ( - // bool isListed, - // uint collateralFactorMantissa - // ) = comptroller.markets(_cEthAddress); - // emit MyLog('ETH Collateral Factor', collateralFactorMantissa); - - // Get the amount of underlying added to your borrow each block - // uint borrowRateMantissa = cToken.borrowRatePerBlock(); - // emit MyLog('Current Borrow Rate', borrowRateMantissa); - - // Get the underlying price in USD from the Price Feed, - // so we can find out the maximum amount of underlying we can borrow. + + // Shortfall > 0 means the account is currently underwater (subject to liquidation). + require(shortfall == 0, "Account is underwater (shortfall > 0)"); + + // Liquidity > 0 means we have available borrowing power. + require(liquidity > 0, "Account has no excess collateral (liquidity = 0)"); + + // 4. Calculate maximum borrowable amount based on USD liquidity. + // liquidity is given in USD scaled by 1e18 (mantissa) uint256 underlyingPrice = priceFeed.getUnderlyingPrice(_cTokenAddress); - uint256 maxBorrowUnderlying = liquidity / underlyingPrice; + + // Max borrowable amount of underlying tokens (scaled to 1e18) + uint256 maxBorrowUnderlyingBase = liquidity / underlyingPrice; - // Borrowing near the max amount will result - // in your account being liquidated instantly - emit MyLog("Maximum underlying Borrow (borrow far less!)", maxBorrowUnderlying); + emit MyLog("Maximum underlying Borrow (in 1e18 units)", maxBorrowUnderlyingBase); + + // - // Borrow underlying + // 5. Borrow the underlying asset. + // WARNING: Borrowing close to maxBorrowUnderlyingBase risks immediate liquidation. uint256 numUnderlyingToBorrow = 10; + uint256 borrowAmount = numUnderlyingToBorrow * 10**_underlyingDecimals; + + uint256 borrowError = cToken.borrow(borrowAmount); + if (borrowError != 0) { + revert BorrowFailed(borrowError); // Use Custom Error + } - // Borrow, check the underlying balance for this contract's address - cToken.borrow(numUnderlyingToBorrow * 10**_underlyingDecimals); - - // Get the borrow balance + // 6. Verify outstanding borrow balance. uint256 borrows = cToken.borrowBalanceCurrent(address(this)); - emit MyLog("Current underlying borrow amount", borrows); + emit MyLog("Current underlying borrow amount (including accrued interest)", borrows); return borrows; } - + + /** + * @notice Repays an outstanding ERC20 token borrow. + * @dev Caller must ensure this contract has sufficient *unlocked* underlying ERC20 tokens. + * @param _erc20Address Address of the underlying ERC20 token. + * @param _cErc20Address Address of the cToken market. + * @param amount The amount of underlying tokens to repay (including accrued interest). + * @return True if successful. + */ function myErc20RepayBorrow( address _erc20Address, address _cErc20Address, @@ -130,85 +160,108 @@ contract MyContract { Erc20 underlying = Erc20(_erc20Address); CErc20 cToken = CErc20(_cErc20Address); + // 1. Approve the cToken contract to pull the underlying tokens from this contract. + // This is necessary because Compound uses transferFrom. underlying.approve(_cErc20Address, amount); + + // 2. Execute the repay. uint256 error = cToken.repayBorrow(amount); - require(error == 0, "CErc20.repayBorrow Error"); + if (error != 0) { + revert RepayFailed(error); // Use Custom Error + } return true; } + // --- ETH BORROWING EXAMPLE (Collateral is an ERC20, Borrow is ETH) --- + + /** + * @notice Supplies an ERC20 token as collateral and then borrows ETH. + * @dev Uses a separate price feed for calculating max borrowable ETH. + * @param _cTokenAddress cToken address corresponding to the ERC20 collateral. + * @param _underlyingAddress The ERC20 token address to supply as collateral. + * @param _underlyingToSupplyAsCollateral The amount of ERC20 to supply. + * @return Current outstanding ETH borrow balance (in Wei). + */ function borrowEthExample( - address payable _cEtherAddress, - address _comptrollerAddress, address _cTokenAddress, address _underlyingAddress, uint256 _underlyingToSupplyAsCollateral - ) public returns (uint) { - CEth cEth = CEth(_cEtherAddress); - Comptroller comptroller = Comptroller(_comptrollerAddress); + ) public returns (uint256) { + CEth cEth = CEth(cEthAddress); + Comptroller comptroller = Comptroller(comptrollerAddress); CErc20 cToken = CErc20(_cTokenAddress); Erc20 underlying = Erc20(_underlyingAddress); - - // Approve transfer of underlying + + // 1. Approve transfer of underlying collateral to the cToken market + // WARNING: Should only approve the amount needed, not MAX_UINT256. underlying.approve(_cTokenAddress, _underlyingToSupplyAsCollateral); - // Supply underlying as collateral, get cToken in return + // 2. Supply underlying ERC20 as collateral, get cToken in return uint256 error = cToken.mint(_underlyingToSupplyAsCollateral); - require(error == 0, "CErc20.mint Error"); + if (error != 0) { + revert MintFailed(error); // Use Custom Error + } - // Enter the market so you can borrow another type of asset - address[] memory cTokens = new address[](1); - cTokens[0] = _cTokenAddress; - uint256[] memory errors = comptroller.enterMarkets(cTokens); + // 3. Enter the market to enable borrowing against the collateral + address[] memory cTokensArray = new address[](1); + cTokensArray[0] = _cTokenAddress; + uint256[] memory errors = comptroller.enterMarkets(cTokensArray); + if (errors[0] != 0) { - revert("Comptroller.enterMarkets failed."); + revert EnterMarketsFailed(errors[0]); // Use Custom Error } - // Get my account's total liquidity value in Compound + // 4. Get liquidity (uint256 error2, uint256 liquidity, uint256 shortfall) = comptroller .getAccountLiquidity(address(this)); + if (error2 != 0) { - revert("Comptroller.getAccountLiquidity failed."); + revert GetLiquidityFailed(error2); // Use Custom Error } - require(shortfall == 0, "account underwater"); - require(liquidity > 0, "account has excess collateral"); - - // Borrowing near the max amount will result - // in your account being liquidated instantly - emit MyLog("Maximum ETH Borrow (borrow far less!)", liquidity); + require(shortfall == 0, "Account is underwater (shortfall > 0)"); + require(liquidity > 0, "Account has no excess collateral (liquidity = 0)"); - // // Get the collateral factor for our collateral - // ( - // bool isListed, - // uint collateralFactorMantissa - // ) = comptroller.markets(_cTokenAddress); - // emit MyLog('Collateral Factor', collateralFactorMantissa); + // Liquidity is expressed in USD (1e18 units). Max ETH borrow is limited by this. + emit MyLog("Maximum ETH Borrow based on liquidity (USD value 1e18)", liquidity); - // // Get the amount of ETH added to your borrow each block - // uint borrowRateMantissa = cEth.borrowRatePerBlock(); - // emit MyLog('Current ETH Borrow Rate', borrowRateMantissa); - - // Borrow a fixed amount of ETH below our maximum borrow amount + // 5. Borrow ETH uint256 numWeiToBorrow = 2000000000000000; // 0.002 ETH - - // Borrow, then check the underlying balance for this contract's address - cEth.borrow(numWeiToBorrow); + + // Borrow ETH (underlying is ETH) + uint256 borrowError = cEth.borrow(numWeiToBorrow); + if (borrowError != 0) { + revert BorrowFailed(borrowError); // Use Custom Error + } uint256 borrows = cEth.borrowBalanceCurrent(address(this)); - emit MyLog("Current ETH borrow amount", borrows); + emit MyLog("Current ETH borrow amount (Wei)", borrows); return borrows; } - function myEthRepayBorrow(address _cEtherAddress, uint256 amount, uint256 gas) + /** + * @notice Repays an outstanding ETH borrow. + * @dev The ETH to repay is sent along with the transaction via `msg.value`. + * @param _cEtherAddress Address of the cETH market. + * @param amount The amount of ETH (in Wei) to repay. + * @param gas The original code included a gas parameter, but fixed gas limits are insecure. + * This parameter is removed in the optimized version. + * @return True if successful. + */ + function myEthRepayBorrow(address _cEtherAddress) public + payable returns (bool) { CEth cEth = CEth(_cEtherAddress); - cEth.repayBorrow{ value: amount, gas: gas }(); + // Repay ETH. The amount sent via `msg.value` is repaid. + // NOTE: If amount is 0, the full borrow balance plus accrued interest is repaid. + cEth.repayBorrow{ value: msg.value }(); return true; } - // Need this to receive ETH when `borrowEthExample` executes + // Fallback function to allow the contract to receive ETH. + // This is required for cEth.borrowEthExample to receive the borrowed ETH. receive() external payable {} }