-
In the video ,Patrick catched the fee transfer break the invariant, but when I test it. why doesn't it catch the invariant break. // SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import {Test, console} from "forge-std/Test.sol";
import {TSwapPool} from "../../src/TSwapPool.sol";
import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol";
contract Handler is Test {
TSwapPool pool;
ERC20Mock mockWeth;
ERC20Mock poolToken;
int256 startingX;
int256 startingY;
int256 public expectDeltaX;
int256 public expectDeltaY;
int256 endingX;
int256 endingY;
int256 public actualDeltaX;
int256 public actualDeltaY;
address lp = makeAddr("liquidityProvider");
address swapper = makeAddr("swapper");
constructor(TSwapPool _pool) {
pool = _pool;
mockWeth = ERC20Mock(pool.getWeth());
poolToken = ERC20Mock(pool.getPoolToken());
}
// we need pooltoken input and get weth output!
// we want wethAmout output : deltaY pool.getInputAmountBasedOnOutput()
// ∆x = (x*outputAmount)/(y - outputAmount)
function poolTokenSwapForWethBasedOnOutputAmount(
uint256 wethAmountOutput
) public {
// determine the wethamount not more than in pools
wethAmountOutput = bound(
wethAmountOutput,
pool.getMinimumWethDepositAmount(),
type(uint64).max
);
if (wethAmountOutput >= mockWeth.balanceOf(address(pool))) {
return;
}
// determine how many pooltoken amount we needed
//getInputAmountBasedOnOutput(outputAmount, inputReserves, outputReserves)
uint256 poolTokenAmount = pool.getInputAmountBasedOnOutput(
wethAmountOutput,
poolToken.balanceOf(address(pool)),
mockWeth.balanceOf(address(pool))
);
if (poolTokenAmount >= type(uint64).max) {
return;
}
startingY = int256(mockWeth.balanceOf(address(pool)));
startingX = int256(poolToken.balanceOf(address(pool)));
expectDeltaY = int256(-1) * int256(wethAmountOutput);
expectDeltaX = int256(poolTokenAmount);
// then we need a swapper to swap!
// swapper must have enough balance!
if (poolToken.balanceOf(swapper) < uint256(expectDeltaX)) {
poolToken.mint(
swapper,
uint256(expectDeltaX) - poolToken.balanceOf(swapper) + 1
);
}
// Start Swap
vm.startPrank(swapper);
poolToken.approve(address(pool), type(uint256).max);
pool.swapExactOutput(
poolToken,
mockWeth,
wethAmountOutput,
uint64(block.timestamp)
);
vm.stopPrank();
// check 2 assets in pool
endingX = int256(poolToken.balanceOf(address(pool)));
endingY = int256(mockWeth.balanceOf(address(pool)));
actualDeltaX = endingX - startingX;
actualDeltaY = endingY - startingY;
}
//q Handler should do what functions to do?
// deposit, swap, withdraw,
function deposit(uint256 wethAmount) public {
wethAmount = bound(
wethAmount,
pool.getMinimumWethDepositAmount(),
type(uint64).max
);
startingX = int256(poolToken.balanceOf(address(pool)));
startingY = int256(mockWeth.balanceOf(address(pool)));
expectDeltaX = int256(
pool.getPoolTokensToDepositBasedOnWeth(wethAmount)
);
expectDeltaY = int256(wethAmount);
// Prank a liquidityProvider to mint
vm.startPrank(lp);
mockWeth.mint(lp, wethAmount);
poolToken.mint(lp, uint256(expectDeltaX));
mockWeth.approve(address(pool), type(uint256).max);
poolToken.approve(address(pool), type(uint256).max);
pool.deposit(
wethAmount,
pool.getMinimumWethDepositAmount(),
uint256(expectDeltaX),
uint64(block.timestamp)
);
vm.stopPrank();
// check the actual deltaX & deltaY
endingX = int256(poolToken.balanceOf(address(pool)));
endingY = int256(mockWeth.balanceOf(address(pool)));
actualDeltaX = endingX - startingX;
actualDeltaY = endingY - startingY;
}
} Here is my invariant.t.sol // SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import {Test} from "forge-std/Test.sol";
import {StdInvariant} from "forge-std/StdInvariant.sol";
import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol";
import {PoolFactory} from "../../src/PoolFactory.sol";
import {TSwapPool} from "../../src/TSwapPool.sol";
import {Handler} from "./Handler.t.sol";
contract Invariant_test is StdInvariant, Test {
// one pool has two asset
ERC20Mock mockWeth;
ERC20Mock poolToken;
// needed contract
PoolFactory poolFactory;
TSwapPool tswapPool; // pooltoken / mockWeth
Handler handler;
int256 public constant STARTING_X = 100 ether;
int256 public constant STARTING_Y = 50 ether;
function setUp() public {
mockWeth = new ERC20Mock();
poolToken = new ERC20Mock();
poolFactory = new PoolFactory(address(mockWeth));
tswapPool = TSwapPool(poolFactory.createPool(address(poolToken)));
// now needs some x & y balance
mockWeth.mint(address(this), uint256(STARTING_Y));
poolToken.mint(address(this), uint256(STARTING_X));
mockWeth.approve(address(tswapPool), type(uint256).max);
poolToken.approve(address(tswapPool), type(uint256).max);
// now needes deposit into pool
tswapPool.deposit(
uint256(STARTING_Y),
uint256(STARTING_Y),
uint256(STARTING_X),
uint64(block.timestamp)
);
handler = new Handler(tswapPool);
bytes4[] memory selectors = new bytes4[](2);
selectors[0] = Handler.deposit.selector;
selectors[1] = Handler.poolTokenSwapForWethBasedOnOutputAmount.selector;
targetSelector(
FuzzSelector({addr: address(handler), selectors: selectors})
);
targetContract(address(handler));
}
function statefulFuzz_constantProductFormulaStayTheSameX() public view {
assertEq(handler.expectDeltaX(), handler.actualDeltaX());
}
function statefulFuzz_constantProductFormulaStayTheSameY() public view {
assertEq(handler.expectDeltaY(), handler.actualDeltaY());
}
} The foundry.toml is [fuzz]
seed = "0x1"
[invariant]
runs = 256
depth = 32
fail_on_revert = true And the message I got always be
|
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 10 replies
-
Isn't Patrick catching the invariant break while running |
Beta Was this translation helpful? Give feedback.
-
Hello @Jaunepr, test for |
Beta Was this translation helpful? Give feedback.
-
He did use |
Beta Was this translation helpful? Give feedback.
-
Here’s what I’ve noticed. Possible Reason: The upper bound for the invariant test is set too high. Initially, the pool contains 50e18 WETH. If the random number ends up being really close to that value, it essentially empties the WETH from the pool. According to the AMM mechanism, the price of WETH increases significantly (which means more pool tokens are required). As a result, when the test runs again, the if (poolTokenAmount > type(uint64).max) {
return;
} This causes the test to exit early, and Solution: The simplest solution is to set a smaller bound on wethAmountOutput = bound(wethAmountOutput, pool.getMinimumWethDepositAmount(), 1e18); |
Beta Was this translation helpful? Give feedback.
Most of the calls to
poolTokenSwapForWethBasedOnOutputAmount
on the handler didn't actually execute but instead, they were returning. Consider checking what @Chinwuba22 pointed out.