The protocol never reads `stakeToken.decimals()` at pool creation or stake time. When a sponsor copies the fork template `minStake = 1e18` for a 6-decimal USDC pool, the human minimum becomes `1e18 / 1e6 = 1e12` USDC (one trillion USDC). The factory accepts the configuration, but every staker including a wallet holding 1,000,000 USDC reverts `BelowMinStake`. The pool clone exists on-chain with `totalEligibleStake == 0` forever; there is no post-init migration path to fix `minStake`.
- Sponsors deploy USDC (6-dec) pools using the in-repo fork example value `1e18` — the only `minStake` constant shown in `BattleChainFactoryIntegration.fork.t.sol`.
- README advertises “ERC20 (standard only)” without documenting that `minStake` is raw units with no decimal validation.
- The opposite misconfiguration (`minStake = 1e6` on an 18-dec token) sets a dust floor, same missing guard, opposite direction.
- Permanent staking liveness DoS for the affected pool clone: `createPool` succeeds but no account can become the first staker.
- Third-party stakers who discover the pool via `PoolCreated` or a UI lose gas on every failed `stake()` attempt.
- The confidence mechanism for that clone is non-functional; the product promise of standard ERC20 staking breaks for the most common L2 stake token class.
```bash
forge build
forge test --match-test test_PoC_factory6DecUSDC_forkTemplateMinStake_bricks1MWhale -vvvv
```
| Path | Role |
|------|------|
| `test/audit/PoC_MDecimals_MinStakeBrick.t.sol` | Standalone PoC (factory path) |
| `test/mocks/MockDecimalERC20.sol` | 6-dec USDC mock |
| `test/fork/BattleChainFactoryIntegration.fork.t.sol` | In-repo source of `minStake=1e18` template |
---
### Reviewer copy-paste setup (clean official contest repo)
All paths relative to **`repo/`**.
**Step 1 — Prerequisites**
```bash
cd repo
git submodule update --init --recursive
forge build
```
**Step 2 — Create the mock file (`MockDecimalERC20.sol`, lines 1–22)**
Create **`test/mocks/MockDecimalERC20.sol`** and paste the **entire file**:
```solidity
pragma solidity 0.8.26;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockDecimalERC20 is ERC20 {
uint8 private immutable _decimals;
constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20(name_, symbol_) {
_decimals = decimals_;
}
function decimals() public view override returns (uint8) {
return _decimals;
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
```
**Step 3 — Create the PoC test file**
Create **`test/audit/PoC_MDecimals_MinStakeBrick.t.sol`** and paste:
```solidity
pragma solidity 0.8.26;
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {ConfidencePool} from "src/ConfidencePool.sol";
import {ConfidencePoolFactory} from "src/ConfidencePoolFactory.sol";
import {IConfidencePool} from "src/interfaces/IConfidencePool.sol";
import {MockAgreement} from "test/mocks/MockAgreement.sol";
import {MockDecimalERC20} from "test/mocks/MockDecimalERC20.sol";
import {BaseConfidencePoolTest} from "test/helpers/BaseConfidencePoolTest.sol";
contract PoC_MDecimals_MinStakeBrickTest is BaseConfidencePoolTest {
uint256 internal constant FORK_TEMPLATE_MIN_STAKE = 1e18;
uint256 internal constant USDC_ONE = 1e6;
uint256 internal constant WHALE_USDC = 1_000_000 * USDC_ONE;
MockDecimalERC20 internal usdc6;
MockAgreement internal sponsorAgreement;
ConfidencePoolFactory internal factory;
address internal agreementOwner = makeAddr("agreementOwner");
function setUp() public override {
super.setUp();
usdc6 = new MockDecimalERC20("USD Coin", "USDC", 6);
sponsorAgreement = new MockAgreement(agreementOwner);
sponsorAgreement.setContractInScope(DEFAULT_SCOPE_ACCOUNT, true);
safeHarborRegistry.setAgreementValid(address(sponsorAgreement), true);
ConfidencePoolFactory factoryImpl = new ConfidencePoolFactory();
factory = ConfidencePoolFactory(
address(
new ERC1967Proxy(
address(factoryImpl),
abi.encodeCall(
ConfidencePoolFactory.initialize,
(address(safeHarborRegistry), address(new ConfidencePool()), moderator)
)
)
)
);
factory.setStakeTokenAllowed(address(usdc6), true);
}
function _scope() internal pure returns (address[] memory accounts) {
accounts = new address[](1);
accounts[0] = DEFAULT_SCOPE_ACCOUNT;
}
function test_PoC_factory6DecUSDC_forkTemplateMinStake_bricks1MWhale() external {
vm.prank(agreementOwner);
ConfidencePool pool = ConfidencePool(
factory.createPool(
address(sponsorAgreement),
address(usdc6),
block.timestamp + 31 days,
FORK_TEMPLATE_MIN_STAKE,
recovery,
_scope()
)
);
assertEq(pool.minStake(), FORK_TEMPLATE_MIN_STAKE);
assertEq(usdc6.decimals(), 6);
usdc6.mint(alice, WHALE_USDC);
vm.startPrank(alice);
usdc6.approve(address(pool), WHALE_USDC);
vm.expectRevert(IConfidencePool.BelowMinStake.selector);
pool.stake(WHALE_USDC);
vm.stopPrank();
assertEq(pool.totalEligibleStake(), 0);
assertEq(usdc6.balanceOf(address(pool)), 0);
}
}
```
**Step 4 — Run**
```bash
forge test --match-test test_PoC_factory6DecUSDC_forkTemplateMinStake_bricks1MWhale -vvvv
```
**Expected result:** `1 passed` — factory `createPool` succeeds with 6-dec token + `minStake=1e18`; 1M USDC `stake` reverts `BelowMinStake`; `totalEligibleStake == 0`.
---
### Full test output and trace
```
[PASS] test_PoC_factory6DecUSDC_forkTemplateMinStake_bricks1MWhale() (gas: 532350)
Traces:
[532350] PoC_MDecimals_MinStakeBrickTest::test_PoC_factory6DecUSDC_forkTemplateMinStake_bricks1MWhale()
├─ [402056] ERC1967Proxy::fallback(..., minStake: 1000000000000000000 [1e18], ...)
│ ├─ [397207] ConfidencePoolFactory::createPool(..., minStake: 1000000000000000000 [1e18], ...) [delegatecall]
│ │ ├─ emit PoolCreated(..., stakeToken: MockDecimalERC20, minStake: 1000000000000000000 [1e18], ...)
│ │ └─ ← [Return] 0x6E50791B4d179fF380Aa28fA9fE90706541873e0
├─ [612] ConfidencePool::minStake() → 1000000000000000000 [1e18]
├─ MockDecimalERC20::decimals() → 6
├─ MockDecimalERC20::mint(alice, 1000000000000 [1e12])
├─ pool.stake(1000000000000 [1e12])
│ ├─ ConfidencePool::stake(1000000000000 [1e12]) [delegatecall]
│ │ └─ ← [Revert] BelowMinStake()
│ └─ ← [Revert] BelowMinStake()
├─ ConfidencePool::totalEligibleStake() → 0
└─ MockDecimalERC20::balanceOf(pool) → 0
Suite result: ok. 1 passed; 0 failed; 0 skipped
```