-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add tests for Erc20PriceOracle base redeem
- Loading branch information
1 parent
9b4a706
commit b635b98
Showing
2 changed files
with
370 additions
and
219 deletions.
There are no files selected for viewing
370 changes: 370 additions & 0 deletions
370
test/foundry/src/concrete/erc20PriceOracle/ERC20PriceOracleReceiptVault.baseRedeem.t.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,370 @@ | ||
// SPDX-License-Identifier: CAL | ||
pragma solidity =0.8.25; | ||
|
||
import {ERC20PriceOracleReceiptVault} from "../../../../../contracts/concrete/vault/ERC20PriceOracleReceiptVault.sol"; | ||
import {ERC20PriceOracleReceiptVaultTest, Vm} from "test/foundry/abstract/ERC20PriceOracleReceiptVaultTest.sol"; | ||
import {TwoPriceOracle} from "../../../../../contracts/oracle/price/TwoPriceOracle.sol"; | ||
import { | ||
LibFixedPointDecimalArithmeticOpenZeppelin, | ||
Math | ||
} from "rain.math.fixedpoint/lib/LibFixedPointDecimalArithmeticOpenZeppelin.sol"; | ||
import {IERC20} from "forge-std/interfaces/IERC20.sol"; | ||
import {Receipt as ReceiptContract} from "../../../../../contracts/concrete/receipt/Receipt.sol"; | ||
import {ZeroAssetsAmount, ZeroReceiver, ZeroOwner} from "../../../../../contracts/abstract/ReceiptVault.sol"; | ||
|
||
contract ERC20PriceOracleReceiptVaultBaseRedeemTest is ERC20PriceOracleReceiptVaultTest { | ||
using LibFixedPointDecimalArithmeticOpenZeppelin for uint256; | ||
|
||
event WithdrawWithReceipt( | ||
address sender, | ||
address receiver, | ||
address owner, | ||
uint256 assets, | ||
uint256 shares, | ||
uint256 id, | ||
bytes receiptInformation | ||
); | ||
|
||
/// Checks that balance owner balance changes after wirthdraw | ||
function checkBalanceChange( | ||
ERC20PriceOracleReceiptVault vault, | ||
address receiver, | ||
address owner, | ||
uint256 id, | ||
uint256 shares, | ||
ReceiptContract receipt, | ||
bytes memory data | ||
) internal { | ||
uint256 initialBalanceOwner = receipt.balanceOf(owner, id); | ||
uint256 assets = shares.fixedPointDiv(id, Math.Rounding.Down); | ||
|
||
vault.setWithdrawId(id); | ||
|
||
// Set up the event expectation for WithdrawWithReceipt | ||
vm.expectEmit(true, true, true, true); | ||
emit WithdrawWithReceipt(owner, receiver, owner, assets, shares, id, data); | ||
|
||
// Call redeem function | ||
vault.redeem(shares, receiver, owner); | ||
|
||
uint256 balanceAfterOwner = receipt.balanceOf(owner, id); | ||
assertEq(balanceAfterOwner, initialBalanceOwner - shares); | ||
} | ||
|
||
/// Checks that balance owner balance does not change after redeem revert | ||
function checkNoBalanceChange( | ||
ERC20PriceOracleReceiptVault vault, | ||
address receiver, | ||
address owner, | ||
uint256 id, | ||
uint256 shares, | ||
ReceiptContract receipt, | ||
bytes memory expectedRevertData | ||
) internal { | ||
uint256 initialBalanceOwner; | ||
uint256 balanceAfterOwner; | ||
|
||
vault.setWithdrawId(id); | ||
|
||
if (owner != address(0)) { | ||
initialBalanceOwner = receipt.balanceOf(owner, id); | ||
} | ||
|
||
// Check if expectedRevertData is provided | ||
if (expectedRevertData.length > 0) { | ||
vm.expectRevert(expectedRevertData); | ||
} else { | ||
vm.expectRevert(); | ||
} | ||
// Call redeem function | ||
vault.redeem(shares, receiver, owner); | ||
|
||
if (owner != address(0)) { | ||
balanceAfterOwner = receipt.balanceOf(owner, id); | ||
} | ||
assertEq(balanceAfterOwner, initialBalanceOwner); | ||
} | ||
|
||
/// Test Base Redeem function | ||
function testBaseRedeem( | ||
uint256 fuzzedKeyAlice, | ||
string memory assetName, | ||
uint256 timestamp, | ||
uint256 assets, | ||
uint256 shares, | ||
uint8 xauDecimals, | ||
uint8 usdDecimals, | ||
uint80 answeredInRound | ||
) external { | ||
// Ensure the fuzzed key is within the valid range for secp256 | ||
address alice = vm.addr((fuzzedKeyAlice % (SECP256K1_ORDER - 1)) + 1); | ||
// Use common decimal bounds for price feeds | ||
// Use 0-20 so we at least have some coverage higher than 18 | ||
usdDecimals = uint8(bound(usdDecimals, 0, 20)); | ||
xauDecimals = uint8(bound(xauDecimals, 0, 20)); | ||
timestamp = bound(timestamp, 0, type(uint32).max); | ||
|
||
vm.warp(timestamp); | ||
TwoPriceOracle twoPriceOracle = createTwoPriceOracle(usdDecimals, usdDecimals, timestamp, answeredInRound); | ||
vm.startPrank(alice); | ||
// Start recording logs | ||
vm.recordLogs(); | ||
ERC20PriceOracleReceiptVault vault = createVault(address(twoPriceOracle), assetName, assetName); | ||
ReceiptContract receipt = getReceipt(); | ||
|
||
vm.mockCall(address(iAsset), abi.encodeWithSelector(IERC20.totalSupply.selector), abi.encode(1e18)); | ||
// Ensure Alice has enough balance and allowance | ||
vm.mockCall(address(iAsset), abi.encodeWithSelector(IERC20.balanceOf.selector, alice), abi.encode(assets)); | ||
|
||
uint256 totalSupply = iAsset.totalSupply(); | ||
// Getting ZeroSharesAmount if bounded from 1 | ||
assets = bound(assets, 2, totalSupply); | ||
vm.mockCall( | ||
address(iAsset), | ||
abi.encodeWithSelector(IERC20.transferFrom.selector, alice, vault, assets), | ||
abi.encode(true) | ||
); | ||
|
||
uint256 oraclePrice = twoPriceOracle.price(); | ||
|
||
vault.deposit(assets, alice, oraclePrice, bytes("")); | ||
|
||
// Bound shares with max avalilable receipt balance | ||
shares = bound(shares, 1, receipt.balanceOf(alice, oraclePrice)); | ||
checkBalanceChange(vault, alice, alice, oraclePrice, shares, receipt, bytes("")); | ||
} | ||
|
||
/// Test Base Redeem function reverts on zero shares | ||
function testBaseRedeemRevertsOnZeroShares( | ||
uint256 fuzzedKeyAlice, | ||
string memory assetName, | ||
uint256 timestamp, | ||
uint256 assets, | ||
uint8 xauDecimals, | ||
uint8 usdDecimals, | ||
uint80 answeredInRound | ||
) external { | ||
// Ensure the fuzzed key is within the valid range for secp256 | ||
address alice = vm.addr((fuzzedKeyAlice % (SECP256K1_ORDER - 1)) + 1); | ||
// Use common decimal bounds for price feeds | ||
// Use 0-20 so we at least have some coverage higher than 18 | ||
usdDecimals = uint8(bound(usdDecimals, 0, 20)); | ||
xauDecimals = uint8(bound(xauDecimals, 0, 20)); | ||
timestamp = bound(timestamp, 0, type(uint32).max); | ||
|
||
vm.warp(timestamp); | ||
TwoPriceOracle twoPriceOracle = createTwoPriceOracle(usdDecimals, usdDecimals, timestamp, answeredInRound); | ||
vm.startPrank(alice); | ||
// Start recording logs | ||
vm.recordLogs(); | ||
ERC20PriceOracleReceiptVault vault = createVault(address(twoPriceOracle), assetName, assetName); | ||
ReceiptContract receipt = getReceipt(); | ||
|
||
vm.mockCall(address(iAsset), abi.encodeWithSelector(IERC20.totalSupply.selector), abi.encode(1e18)); | ||
// Ensure Alice has enough balance and allowance | ||
vm.mockCall(address(iAsset), abi.encodeWithSelector(IERC20.balanceOf.selector, alice), abi.encode(assets)); | ||
|
||
uint256 totalSupply = iAsset.totalSupply(); | ||
// Getting ZeroSharesAmount if bounded from 1 | ||
assets = bound(assets, 2, totalSupply); | ||
vm.mockCall( | ||
address(iAsset), | ||
abi.encodeWithSelector(IERC20.transferFrom.selector, alice, vault, assets), | ||
abi.encode(true) | ||
); | ||
|
||
uint256 oraclePrice = twoPriceOracle.price(); | ||
|
||
vault.deposit(assets, alice, oraclePrice, bytes("")); | ||
|
||
checkNoBalanceChange( | ||
vault, alice, alice, oraclePrice, 0, receipt, abi.encodeWithSelector(ZeroAssetsAmount.selector) | ||
); | ||
} | ||
|
||
/// Test Base Redeem function reverts on zero receiver | ||
function testBaseRedeemRevertsOnZeroReceiver( | ||
uint256 fuzzedKeyAlice, | ||
string memory assetName, | ||
uint256 timestamp, | ||
uint256 assets, | ||
uint256 shares, | ||
uint8 xauDecimals, | ||
uint8 usdDecimals, | ||
uint80 answeredInRound | ||
) external { | ||
// Ensure the fuzzed key is within the valid range for secp256 | ||
address alice = vm.addr((fuzzedKeyAlice % (SECP256K1_ORDER - 1)) + 1); | ||
// Use common decimal bounds for price feeds | ||
// Use 0-20 so we at least have some coverage higher than 18 | ||
usdDecimals = uint8(bound(usdDecimals, 0, 20)); | ||
xauDecimals = uint8(bound(xauDecimals, 0, 20)); | ||
timestamp = bound(timestamp, 0, type(uint32).max); | ||
|
||
vm.warp(timestamp); | ||
TwoPriceOracle twoPriceOracle = createTwoPriceOracle(usdDecimals, usdDecimals, timestamp, answeredInRound); | ||
vm.startPrank(alice); | ||
// Start recording logs | ||
vm.recordLogs(); | ||
ERC20PriceOracleReceiptVault vault = createVault(address(twoPriceOracle), assetName, assetName); | ||
ReceiptContract receipt = getReceipt(); | ||
|
||
vm.mockCall(address(iAsset), abi.encodeWithSelector(IERC20.totalSupply.selector), abi.encode(1e18)); | ||
// Ensure Alice has enough balance and allowance | ||
vm.mockCall(address(iAsset), abi.encodeWithSelector(IERC20.balanceOf.selector, alice), abi.encode(assets)); | ||
|
||
uint256 totalSupply = iAsset.totalSupply(); | ||
// Getting ZeroSharesAmount if bounded from 1 | ||
assets = bound(assets, 2, totalSupply); | ||
vm.mockCall( | ||
address(iAsset), | ||
abi.encodeWithSelector(IERC20.transferFrom.selector, alice, vault, assets), | ||
abi.encode(true) | ||
); | ||
|
||
uint256 oraclePrice = twoPriceOracle.price(); | ||
|
||
vault.deposit(assets, alice, oraclePrice, bytes("")); | ||
// Bound shares with max avalilable receipt balance | ||
shares = bound(shares, 1, receipt.balanceOf(alice, oraclePrice)); | ||
|
||
checkNoBalanceChange( | ||
vault, address(0), alice, oraclePrice, shares, receipt, abi.encodeWithSelector(ZeroReceiver.selector) | ||
); | ||
} | ||
|
||
/// Test Base Redeem function reverts on zero owner | ||
function testBaseRedeemRevertsOnZeroOwner( | ||
uint256 fuzzedKeyAlice, | ||
string memory assetName, | ||
uint256 timestamp, | ||
uint256 assets, | ||
uint256 shares, | ||
uint8 xauDecimals, | ||
uint8 usdDecimals, | ||
uint80 answeredInRound | ||
) external { | ||
// Ensure the fuzzed key is within the valid range for secp256 | ||
address alice = vm.addr((fuzzedKeyAlice % (SECP256K1_ORDER - 1)) + 1); | ||
// Use common decimal bounds for price feeds | ||
// Use 0-20 so we at least have some coverage higher than 18 | ||
usdDecimals = uint8(bound(usdDecimals, 0, 20)); | ||
xauDecimals = uint8(bound(xauDecimals, 0, 20)); | ||
timestamp = bound(timestamp, 0, type(uint32).max); | ||
|
||
vm.warp(timestamp); | ||
TwoPriceOracle twoPriceOracle = createTwoPriceOracle(usdDecimals, usdDecimals, timestamp, answeredInRound); | ||
vm.startPrank(alice); | ||
// Start recording logs | ||
vm.recordLogs(); | ||
ERC20PriceOracleReceiptVault vault = createVault(address(twoPriceOracle), assetName, assetName); | ||
ReceiptContract receipt = getReceipt(); | ||
|
||
vm.mockCall(address(iAsset), abi.encodeWithSelector(IERC20.totalSupply.selector), abi.encode(1e18)); | ||
// Ensure Alice has enough balance and allowance | ||
vm.mockCall(address(iAsset), abi.encodeWithSelector(IERC20.balanceOf.selector, alice), abi.encode(assets)); | ||
|
||
uint256 totalSupply = iAsset.totalSupply(); | ||
// Getting ZeroSharesAmount if bounded from 1 | ||
assets = bound(assets, 2, totalSupply); | ||
vm.mockCall( | ||
address(iAsset), | ||
abi.encodeWithSelector(IERC20.transferFrom.selector, alice, vault, assets), | ||
abi.encode(true) | ||
); | ||
|
||
uint256 oraclePrice = twoPriceOracle.price(); | ||
|
||
vault.deposit(assets, alice, oraclePrice, bytes("")); | ||
// Bound shares with max avalilable receipt balance | ||
shares = bound(shares, 1, receipt.balanceOf(alice, oraclePrice)); | ||
|
||
checkNoBalanceChange(vault, alice, address(0), oraclePrice, shares, receipt, bytes("")); | ||
} | ||
|
||
/// Test Base PreviewRedeem returns correct assets | ||
function testBasePreviewRedeem( | ||
uint256 fuzzedKeyAlice, | ||
string memory assetName, | ||
uint256 timestamp, | ||
uint256 shares, | ||
uint8 xauDecimals, | ||
uint8 usdDecimals, | ||
uint80 answeredInRound | ||
) external { | ||
// Ensure the fuzzed key is within the valid range for secp256 | ||
address alice = vm.addr((fuzzedKeyAlice % (SECP256K1_ORDER - 1)) + 1); | ||
// Use common decimal bounds for price feeds | ||
// Use 0-20 so we at least have some coverage higher than 18 | ||
usdDecimals = uint8(bound(usdDecimals, 0, 20)); | ||
xauDecimals = uint8(bound(xauDecimals, 0, 20)); | ||
timestamp = bound(timestamp, 0, type(uint32).max); | ||
shares = bound(shares, 1, type(uint64).max); | ||
|
||
vm.warp(timestamp); | ||
TwoPriceOracle twoPriceOracle = createTwoPriceOracle(usdDecimals, usdDecimals, timestamp, answeredInRound); | ||
|
||
// Prank as Alice to grant role | ||
vm.startPrank(alice); | ||
ERC20PriceOracleReceiptVault vault = createVault(address(twoPriceOracle), assetName, assetName); | ||
|
||
uint256 oraclePrice = twoPriceOracle.price(); | ||
uint256 assets = shares.fixedPointDiv(oraclePrice, Math.Rounding.Down); | ||
vault.setWithdrawId(oraclePrice); | ||
|
||
uint256 ResultAssets = vault.previewRedeem(shares); | ||
assertEq(assets, ResultAssets); | ||
// Stop the prank | ||
vm.stopPrank(); | ||
} | ||
|
||
/// Test Base Redeem function with more than balance | ||
function testBaseRedeemMoreThanBalance( | ||
uint256 fuzzedKeyAlice, | ||
string memory assetName, | ||
uint256 timestamp, | ||
uint256 assets, | ||
uint256 sharesToRedeem, | ||
uint8 xauDecimals, | ||
uint8 usdDecimals, | ||
uint80 answeredInRound | ||
) external { | ||
// Ensure the fuzzed key is within the valid range for secp256 | ||
address alice = vm.addr((fuzzedKeyAlice % (SECP256K1_ORDER - 1)) + 1); | ||
// Use common decimal bounds for price feeds | ||
// Use 0-20 so we at least have some coverage higher than 18 | ||
usdDecimals = uint8(bound(usdDecimals, 0, 20)); | ||
xauDecimals = uint8(bound(xauDecimals, 0, 20)); | ||
timestamp = bound(timestamp, 0, type(uint32).max); | ||
|
||
vm.warp(timestamp); | ||
TwoPriceOracle twoPriceOracle = createTwoPriceOracle(usdDecimals, usdDecimals, timestamp, answeredInRound); | ||
vm.startPrank(alice); | ||
// Start recording logs | ||
vm.recordLogs(); | ||
ERC20PriceOracleReceiptVault vault = createVault(address(twoPriceOracle), assetName, assetName); | ||
ReceiptContract receipt = getReceipt(); | ||
|
||
vm.mockCall(address(iAsset), abi.encodeWithSelector(IERC20.totalSupply.selector), abi.encode(1e18)); | ||
// Ensure Alice has enough balance and allowance | ||
vm.mockCall(address(iAsset), abi.encodeWithSelector(IERC20.balanceOf.selector, alice), abi.encode(assets)); | ||
|
||
uint256 totalSupply = iAsset.totalSupply(); | ||
// Getting ZeroSharesAmount if bounded from 1 | ||
assets = bound(assets, 2, totalSupply); | ||
vm.mockCall( | ||
address(iAsset), | ||
abi.encodeWithSelector(IERC20.transferFrom.selector, alice, vault, assets), | ||
abi.encode(true) | ||
); | ||
|
||
uint256 oraclePrice = twoPriceOracle.price(); | ||
|
||
vault.deposit(assets, alice, oraclePrice, bytes("")); | ||
uint256 availableReceiptBalance = receipt.balanceOf(alice, oraclePrice); | ||
|
||
// Make sure sharesToRedeem is more than available balance | ||
sharesToRedeem = bound(sharesToRedeem, availableReceiptBalance + 1, type(uint256).max); | ||
checkNoBalanceChange(vault, alice, alice, oraclePrice, sharesToRedeem, receipt, bytes("")); | ||
} | ||
} |
Oops, something went wrong.