You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
InvestToken contract has a vulnerability in its deposit function due to non-atomic price calculations, allowing for potential manipulation of share prices between calculation and minting.
Finding Description
The deposit function in the InvestToken contract calculates the number of shares to mint based on the current price from the YieldOracle. However, this calculation and the subsequent minting of shares are not atomic. This creates a window where the price can change, allowing an attacker to manipulate the share price. This breaks the security guarantee of consistent share-to-asset conversion rates as per the ERC4626 standard. A malicious actor could exploit this by timing their deposits around known price updates, leading to an unfair advantage and potential financial loss for the protocol.
function assetsToShares(uint256assets) externalviewreturns (uint256) {
return Math.mulDiv(assets, 10**18, currentPrice);
// audit-iss - Price can change between read and use
}
The deposit() function calculates shares using the current price from YieldOracle, but between the share calculation and actual minting, the oracle price could change if updatePrice() is called, leading to inconsistent share amounts.
This vulnerability breaks the security guarantee of maintaining a stable 1:1 value relationship between USDE stablecoin and its underlying collateral through the InvestToken (EUI) wrapper.
Impact Explanation
The impact is significant because it allows for immediate value extraction through price manipulation. This breaks invariant of the ERC4626 standard, leading to potential financial losses for the protocol and its users.
Scenario:
User calls deposit() with X assets
convertToShares() calculates shares using price P1
Oracle price updates to P2 before minting completes
Actual shares minted don't match the expected conversion rate
Attack Scenario
The exploit path works through these steps:
A user deposits USDE at current price (e.g. 1.0)
The oracle updates prices upward in two steps (e.g. 1.1 then 1.2)
The user redeems their EUI shares using the previous price (1.1)
The key issue occurs in the InvestToken contract where:
Deposits use currentPrice from YieldOracle (convertToShares)
Withdrawals use previousPrice from YieldOracle (convertToAssets)
This creates a systemic arbitrage opportunity where users can extract excess USDE by timing their actions around oracle price updates. The test demonstrates gaining 100 USDE profit from a 1000 USDE deposit through this mechanism.
The vulnerability requires active exploitation through:
Monitoring oracle update schedules
Timing deposits before price increases
Executing withdrawals after prices update
Repeating the cycle to compound profits
This breaks the protocol's economic security by allowing unauthorized value extraction, potentially depleting reserves and destabilizing the USDE peg.
The root cause lies in using different prices for deposits vs withdrawals, creating an exploitable price differential that skilled attackers can leverage for guaranteed profits.
Likelihood Explanation
The likelihood of occurring is moderate to high, especially if oracle price updates are predictable or controlled by a malicious actor. Users with knowledge of upcoming price changes can exploit this vulnerability to gain an unfair advantage.
Proof of Concept
This PoC demonstrates:
Price manipulation through oracle updates
Deposit execution and share calculation
Profit verification through asset/share conversion
// SPDX-License-Identifier: MITpragma solidity^0.8.21;
import {Test} from"forge-std/Test.sol";
import {console2} from"forge-std/console2.sol";
import {InvestToken} from"../src/InvestToken.sol";
import {USDE} from"../src/USDE.sol";
import {IUSDE} from"../src/interfaces/IUSDE.sol";
import {Validator} from"../src/Validator.sol";
import {YieldOracle} from"../src/YieldOracle.sol";
import {ERC1967Proxy} from"@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
contractSharePriceManipulationTestisTest {
InvestToken public investToken;
USDE public usde;
Validator public validator;
YieldOracle public oracle;
address admin =address(1);
address alice =address(2);
address bob =address(3);
address oracleOperator =address(4);
function setUp() public {
vm.startPrank(admin);
// Deploy validator
validator =newValidator(admin, admin, admin);
// Deploy USDE implementation and proxy
USDE usdeImpl =newUSDE(validator);
bytesmemory usdeData =abi.encodeWithSelector(USDE.initialize.selector, admin);
ERC1967Proxy usdeProxy =newERC1967Proxy(address(usdeImpl), usdeData);
usde =USDE(address(usdeProxy));
// Deploy oracle
oracle =newYieldOracle(admin, oracleOperator);
// Deploy InvestToken implementation and proxy
InvestToken investTokenImpl =newInvestToken(validator, IUSDE(address(usde)));
bytesmemory investTokenData =abi.encodeWithSelector(
InvestToken.initialize.selector,
"EuroDollar Invest",
"EUI",
admin,
oracle
);
ERC1967Proxy investTokenProxy =newERC1967Proxy(address(investTokenImpl), investTokenData);
investToken =InvestToken(address(investTokenProxy));
// Grant required roles
usde.grantRole(usde.MINT_ROLE(), admin);
usde.grantRole(usde.BURN_ROLE(), admin);
usde.grantRole(usde.MINT_ROLE(), address(investToken));
usde.grantRole(usde.BURN_ROLE(), address(investToken));
investToken.grantRole(investToken.MINT_ROLE(), admin);
investToken.grantRole(investToken.BURN_ROLE(), admin);
// Whitelist addresses
validator.whitelist(alice);
validator.whitelist(bob);
validator.whitelist(address(investToken));
vm.stopPrank();
// Fund Alice
vm.startPrank(admin);
usde.mint(alice, 1000e18);
vm.stopPrank();
}
function testSharePriceManipulation() public {
emitlog_string("\n=== Initial State ===");
emitlog_named_decimal_uint("Alice USDE Balance", usde.balanceOf(alice), 18);
emitlog_named_decimal_uint("Initial Oracle Price", oracle.currentPrice(), 18);
// Alice deposits USDE at initial price
vm.startPrank(alice);
usde.approve(address(investToken), type(uint256).max);
uint256 depositAmount =1000e18;
uint256 sharesReceived = investToken.deposit(depositAmount, alice);
vm.stopPrank();
emitlog_string("\n=== After Initial Deposit ===");
emitlog_named_decimal_uint("Shares Received", sharesReceived, 18);
// Update price twice to get higher previousPrice
vm.warp(block.timestamp+ oracle.updateDelay() +1);
vm.startPrank(oracleOperator);
oracle.updatePrice(1.1e18);
vm.warp(block.timestamp+ oracle.commitDelay() +1);
oracle.commitPrice();
vm.warp(block.timestamp+ oracle.updateDelay() +1);
oracle.updatePrice(1.2e18);
vm.warp(block.timestamp+ oracle.commitDelay() +1);
oracle.commitPrice();
emitlog_named_decimal_uint("New Oracle Price", oracle.currentPrice(), 18);
vm.stopPrank();
// Alice redeems shares after price increases
vm.startPrank(alice);
uint256 redeemedAssets = investToken.redeem(sharesReceived, alice, alice);
vm.stopPrank();
emitlog_string("\n=== After Redemption ===");
emitlog_named_decimal_uint("Redeemed Assets", redeemedAssets, 18);
assertGt(redeemedAssets, depositAmount, "No profit generated from price manipulation");
emitlog_named_decimal_uint("Profit in USDE", redeemedAssets - depositAmount, 18);
}
}
We can see the test reveals a significant vulnerability in the system. Here's what's happening:
Alice deposits 1000 USDE at the initial price of 1.0, receiving 1000 shares
Price increases through two updates to 1.2 USDE per share
When Alice redeems her shares, she receives 1100 USDE back
This results in a pure profit of 100 USDE
This demonstrates a critical issue where users can profit from the price oracle's mechanism by timing their deposits and withdrawals around price updates. The root cause is that the system uses different prices for deposits (currentPrice) and withdrawals (previousPrice), creating an arbitrage opportunity.
This vulnerability could be exploited by users to extract value from the system, which is particularly concerning for a stablecoin protocol.
Detailed step-by-step confirmation of the price manipulation strategy:
Initial setup shows Alice with 1000 USDE and oracle price at 1.0
Alice deposits 1000 USDE and receives exactly 1000 shares (1:1 ratio)
Oracle price updates occur:
First update to 1.1
Second update to 1.2
When Alice redeems her 1000 shares:
YieldOracle.sharesToAssets() calculates using previousPrice (1.1)
Returns 1100 USDE (1000 * 1.1)
USDE.mint creates 1100 USDE for Alice
The traces show the exact flow of tokens and price updates, confirming that users can generate risk-free profits by timing their actions around oracle price updates. This creates a clear arbitrage opportunity in the protocol's price mechanism.
Recommendation (Optional)
Ensure that the price used for share calculation is locked at the time of the deposit call, preventing any manipulation between calculation and minting. Implement a price snapshot mechanism
function deposit(uint256 assets, address receiver) public returns (uint256 shares) {
+ uint256 priceSnapshot = yieldOracle.currentPrice();- shares = convertToShares(assets);+ shares = convertToSharesAtPrice(assets, priceSnapshot); // Use a snapshot of the current price to ensure atomicity
usde.burn(msg.sender, assets);
_mint(receiver, shares);
emit Deposit(msg.sender, receiver, assets, shares);
}
+ function convertToSharesAtPrice(uint256 assets, uint256 price) internal pure returns (uint256) {+ return Math.mulDiv(assets, 10**18, price); // Calculate shares using the price snapshot+ }
The text was updated successfully, but these errors were encountered:
Looks like a GPT generated submission. If there are any issues related to what is mentioned (value extraction due to conversion) it's been mentioned in prev issues
Github username: @emerald7017
Twitter username: --
Submission hash (on-chain): 0xd4b2987f0bc50850b16d8e87ec987ff73ea72b34671d878f8500a1fddf553d26
Severity: high
Description:
Summary
InvestToken contract has a vulnerability in its deposit function due to non-atomic price calculations, allowing for potential manipulation of share prices between calculation and minting.
Finding Description
The deposit function in the InvestToken contract calculates the number of shares to mint based on the current price from the YieldOracle. However, this calculation and the subsequent minting of shares are not atomic. This creates a window where the price can change, allowing an attacker to manipulate the share price. This breaks the security guarantee of consistent share-to-asset conversion rates as per the ERC4626 standard. A malicious actor could exploit this by timing their deposits around known price updates, leading to an unfair advantage and potential financial loss for the protocol.
InvestToken.sol>L203-L204
The bug is in the
deposit()
function in InvestToken.sol where there's a potential mismatch between shares calculation:InvestToken.sol>L243-L248
YieldOracle.sol>L174-L175
The
deposit()
function calculates shares using the current price from YieldOracle, but between the share calculation and actual minting, the oracle price could change ifupdatePrice()
is called, leading to inconsistent share amounts.Impact Explanation
The impact is significant because it allows for immediate value extraction through price manipulation. This breaks invariant of the ERC4626 standard, leading to potential financial losses for the protocol and its users.
Scenario:
deposit()
with X assetsconvertToShares()
calculates shares using price P1Attack Scenario
The exploit path works through these steps:
The key issue occurs in the InvestToken contract where:
convertToShares
)convertToAssets
)This creates a systemic arbitrage opportunity where users can extract excess USDE by timing their actions around oracle price updates. The test demonstrates gaining 100 USDE profit from a 1000 USDE deposit through this mechanism.
The vulnerability requires active exploitation through:
This breaks the protocol's economic security by allowing unauthorized value extraction, potentially depleting reserves and destabilizing the USDE peg.
The root cause lies in using different prices for deposits vs withdrawals, creating an exploitable price differential that skilled attackers can leverage for guaranteed profits.
Likelihood Explanation
The likelihood of occurring is moderate to high, especially if oracle price updates are predictable or controlled by a malicious actor. Users with knowledge of upcoming price changes can exploit this vulnerability to gain an unfair advantage.
Proof of Concept
This PoC demonstrates:
The Logs
We can see the test reveals a significant vulnerability in the system. Here's what's happening:
This demonstrates a critical issue where users can profit from the price oracle's mechanism by timing their deposits and withdrawals around price updates. The root cause is that the system uses different prices for deposits (currentPrice) and withdrawals (previousPrice), creating an arbitrage opportunity.
This vulnerability could be exploited by users to extract value from the system, which is particularly concerning for a stablecoin protocol.
Detailed step-by-step confirmation of the price manipulation strategy:
Initial setup shows Alice with 1000 USDE and oracle price at 1.0
Alice deposits 1000 USDE and receives exactly 1000 shares (1:1 ratio)
Oracle price updates occur:
When Alice redeems her 1000 shares:
YieldOracle.sharesToAssets()
calculates usingpreviousPrice
(1.1)The traces show the exact flow of tokens and price updates, confirming that users can generate risk-free profits by timing their actions around oracle price updates. This creates a clear arbitrage opportunity in the protocol's price mechanism.
Recommendation (Optional)
Ensure that the price used for share calculation is locked at the time of the deposit call, preventing any manipulation between calculation and minting. Implement a price snapshot mechanism
The text was updated successfully, but these errors were encountered: