Markdown Converter
Agent skill for markdown-converter
generic skill
Sign in to like and favorite skills
Implement the on‑chain system around the already‑finished
Core.sol and CoreTypes.sol, plus Storage libraries.
Provide:
Pool.sol) that bridges Storage and Core and performs all side‑effectsUse Foundry. Keep imports compatible with the specified Aave libs.
root/ foundry.toml remappings.txt .github/workflows/ci.yml .prettierrc .solhint.json .gitignore README.md /contracts /core Core.sol CoreTypes.sol /storage PoolStorage.sol StorageCodec.sol SeriesStorage.sol QueueStorage.sol // optional minimal queue state (or embed in PoolStorage) /interfaces IPool.sol IPoolAdmin.sol IOracle.sol ICToken.sol ILPToken.sol /tokens CToken1155.sol LPToken.sol /pool Pool.sol PoolAdmin.sol // governor/ops functions PoolFactory.sol // instantiate pools/products if we later need >1 product /oracle OracleAdapter.sol // wraps external truth source to IOracle ProportionalModel.sol // optional g(data)→ratio provider interface + mock /libs TokenIdLib.sol AccountingLib.sol // non-core helpers that are not math (e.g. event checks) RBCLib.sol // RBC enforcement helpers (pure/view) SafeTransferLib.sol // thin wrapper over OZ SafeERC20 for brevity Errors.sol Events.sol /script Deploy.s.sol CreateSeries.s.sol Seed.s.sol /test /unit Core_Quote.t.sol Pool_Buy.t.sol Pool_Settle.t.sol Pool_Mature.t.sol LP_DepositWithdraw.t.sol CT_Redeem.t.sol RBC_Guards.t.sol Pause_Cooldown.t.sol Activation_Gating.t.sol Decimals_6_18.t.sol /property Invariants.t.sol // forge-std invariant framework /mocks MockERC20.sol MockOracle.sol MockProportionalModel.sol MockFailingERC20.sol // to test transfer failures
Compiler: Solidity 0.8.24 (same as Core). Enable via
foundry.toml.
Libraries (as requested in Core):
import {WadRayMath} from "@aave/protocol/libraries/math/WadRayMath.sol"; import {PercentageMath} from "@aave/protocol/libraries/math/PercentageMath.sol"; import {MathUtils} from "@aave/protocol/libraries/math/MathUtils.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
Also use:
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
remappings.txt example:
@openzeppelin/=lib/openzeppelin-contracts/ @aave/=lib/aave-v3-core/ // or the actual monorepo path that contains protocol/libraries/math/*
/contracts/interfaces)Implement the following minimal external APIs (function sets match previous design):
IPool.sol
interface IPool { // Views function product() external view returns (PoolStorage.ProductConfig memory); function getSeries(bytes32 seriesId) external view returns (PoolStorage.SeriesState memory); function previewQuote(uint256 tenorDays) external view returns (uint256 pTWad, uint256 uWad); function utilization() external view returns (uint256 uWad); function nav() external view returns (uint256); function freeCapital() external view returns (uint256); function maxLiability() external view returns (uint256); // Primary market function buy(bytes32 seriesId, uint256 notional, address receiver) external returns (uint256 tokenId, uint256 premiumPaid); // Settlement / Maturity function settle(bytes32 seriesId) external returns (uint256 payoutRatioWad, uint256 payTotal); function mature(bytes32 seriesId) external returns (uint256 unlocked); function redeem(uint256 tokenId, uint256 amount) external returns (uint256 paid); // LP function deposit(uint256 amount, address receiver) external returns (uint256 shares); function withdraw(uint256 shares, address receiver) external returns (uint256 amountPaid); }
IPoolAdmin.sol
interface IPoolAdmin { function pause() external; function unpause() external; function setCooldown(uint64 untilTs) external; function setFees(uint256 feePremWad, uint256 feeMgmtAprWad) external; function setOracle(address oracle) external; function setRBCParams(bytes32 bucket, uint256 rhoWad, uint256 bufferWad) external; function createSeries(uint64 maturity, uint256 rMaxWad) external returns (bytes32 seriesId); }
IOracle.sol
interface IOracle { function eventConfirmed(bytes32 seriesId) external view returns (bool ok); function payoutRatio(bytes32 seriesId) external view returns (uint256 rWad, uint64 eventTime); }
(ERC1155 extension)ICToken.sol
interface ICToken { function mint(address to, uint256 id, uint256 amount) external; function burnFrom(address from, uint256 id, uint256 amount) external; function seriesOf(uint256 tokenId) external view returns (bytes32); function activationStartOf(uint256 tokenId) external view returns (uint64); }
(ERC20 extension)ILPToken.sol
interface ILPToken { function mint(address to, uint256 amount) external; function burn(address from, uint256 amount) external; }
Add event and error definitions in a shared
Events.sol and Errors.sol library for consistency.
/contracts/tokens)CToken1155.sol
ERC1155 with:
onlyPool minter/burner
Metadata storage:
mapping(uint256 => bytes32) private _seriesOf;mapping(uint256 => uint64) private _activationStart;function mint(address to, uint256 id, uint256 amount) external onlyPool
function burnFrom(address from, uint256 id, uint256 amount) external onlyPool
function seriesOf(uint256 id) external view returns (bytes32)
function activationStartOf(uint256 id) external view returns (uint64)
No enumerable requirement. Redemption is pull‑based via Pool.
Token ID is built off
TokenIdLib.ctTokenId(seriesId, activationStart).
LPToken.sol
onlyPool mint/burn. No fee logic./contracts/libs)
(as designed)TokenIdLib.sol
library TokenIdLib { function seriesId(bytes32 productId, uint64 maturity) internal pure returns (bytes32) { return keccak256(abi.encodePacked(productId, maturity)); } function ctTokenId(bytes32 seriesId, uint64 activationStart) internal pure returns (uint256) { return uint256(keccak256(abi.encodePacked(seriesId, activationStart))); } }
RBCLib.sol
function requiredCapital(PoolStorage.RBC storage rbc) internal view returns (uint256)
Sum rhoWad[bucket]*L_bucket / 1e18, then multiply by bufferWad. Returns asset units by design: we already store L_bucket in asset units, rhoWad and bufferWad are WAD.checkSolvency(PoolStorage.Layout storage L) returns (bool ok) comparing cTotal vs required capital.AccountingLib.sol
SafeTransferLib.sol
SafeERC20 to reduce repetitive boilerplate in Pool.sol.Errors.sol
Mirror CoreTypes errors for Pool‑level reverts:
error OnlyPool(); error OnlyGovernor(); error Cooldown(); error NotConfirmed(); error NotEligible(uint64 activationStart, uint64 eventTime); error ZeroAmount(); error BadOracle(); etc.Events.sol
Standardize:
event Bought(address indexed buyer, bytes32 indexed seriesId, uint256 tokenId, uint256 notional, uint256 premium, uint256 uNewWad);event Settled(bytes32 indexed seriesId, uint256 payoutRatioWad, uint256 payTotal, uint64 eventTime);event Matured(bytes32 indexed seriesId, uint256 unlocked);event Redeemed(address indexed account, uint256 indexed tokenId, uint256 amount, uint256 paid);event Deposit(address indexed lp, uint256 amount, uint256 shares);event Withdraw(address indexed lp, uint256 shares, uint256 amount, bool queued);event Paused(); event Unpaused();/contracts/pool/Pool.sol)Responsibilities:
Core → apply deltas with StorageCodecKey fields:
GOVERNOR_ROLE, OPERATOR_ROLEKey functions:
Views
product() returns PoolStorage.ProductConfiggetSeries(seriesId) returns current SeriesStatepreviewQuote(tenorDays) calls Core.quote(snapshot) and returns (pTWad, uWad)utilization(), nav(), freeCapital(), maxLiability()Primary
buy(seriesId, notional, receiver)
require(block.timestamp >= L.cooldownUntil) to avoid selling in cooldown
Build snapshots:
poolBytes, seriesBytes, productBytes
Call
Core.buy(...)
Effects:
Transfer
premium from msg.sender to Pool
Transfer
feeToTreasury to treasury
Apply deltas:
applyPoolDelta, applySeriesDelta
Mint CT:
tokenId = TokenIdLib.ctTokenId(seriesId, activationStart)CToken1155.mint(receiver, tokenId, notional)seriesOf[tokenId] and activationStartOf[tokenId] is handled inside tokenEmit
Bought
Return
(tokenId, premium)
Settlement
settle(seriesId)
require(oracle.eventConfirmed(seriesId))(r, eventTime) = oracle.payoutRatio(seriesId)Core.settle(...) with nowTs=block.timestamp, eventTime, rpaused = true, cooldownUntil)Settled(r, payTotal)Maturity
mature(seriesId)
block.timestamp >= maturityCore.mature(...)MaturedRedeem
redeem(tokenId, amount)
seriesId and activationStart via CTseries.status == SettledactivationStart <= series.eventTime (eligibility)pay = amount * series.payoutRatioWad / WAD (downward rounding)pay to msg.sender (pull model)RedeemedLP
deposit(amount, receiver)
amount > 0Core.deposit(...)amountmintShares to receiverDepositwithdraw(shares, receiver)
shares > 0Core.withdraw(...) with inputs (shares, sTotal, nav, cFree)burnShares from msg.senderpayAmount to receiverqueued, enqueue the remainder in WithdrawQueue (see 3.5)WithdrawGuards
Before selling (
buy) also enforce RBC if enabled:
require(RBCLib.checkSolvency(L)) prior to opening sales; otherwise pause.Admin
PoolAdmin.sol that mutate Storage fields (fees, oracle, RBC params, createSeries). Restrict with AccessControl.Implement a minimal FIFO queue library or inline in the Pool:
Storage:
struct Request { address lp; uint256 shares; uint256 navAmount; }mapping(uint256 => Request) q; uint256 head; uint256 tail;On
withdraw when queued == true:
(lp, remainingShares, remainingNAV) after partial immediate payment and burn only the portion corresponding to paid amount. Alternatively, burn full shares upfront and mint an IOU ERC1155 representing queue position. MVP: keep simple and burn only the paid‑corresponding portion as Core suggested; enqueue unpaid portion in NAV terms and re‑price at settlement with a fairness rule documented.settleQueue() callable by anyone:
cFree > 0 and queue not empty, pay out requests FIFO up to cFree.Document how partial fills map to LP shares. MVP: keep invariant “shares burned equals NAV paid / current p” to avoid drift.
Store per‑bucket
rhoWad and aggregate L_by_bucket in PoolStorage.RBC.
Update
L_by_bucket only when applying SeriesDelta.dCLocked.
requiredCapital = sum_b( rho[b] * L_b ) * bufferWad / 1e36 (two WAD multiplications).
If
cTotal < requiredCapital, enforce:
Expose read views for monitoring.
OracleAdapter.sol conforms to IOracle and shields Pool from external feeds:
For binary payouts,
payoutRatio returns (1e18, eventTime) when condition is met.
For proportional/capped, read external model
ProportionalModel.g(seriesId) and cap by rMax.
Trust boundaries:
eventTime != 0 and not in the futureInclude a mock for tests.
If we foresee multiple products/pools:
PoolFactory.sol that deploys a new Pool with product config and connects CT/LP tokens.Solidity: 0.8.24, SPDX identifiers, NatSpec for external/public functions.
CEI pattern strictly in
Pool.sol:
StorageCodec.apply*Deltabuy, where premium must be transferred before mint to keep accounting consistent; still do it safely and never transfer after external mint calls)Use
custom errors not revert strings.
Use OZ
SafeERC20 and check return values.
Rounding:
U_new) and locks: round upfoundry.toml
[profile.default] solc_version = "0.8.24" optimizer = true optimizer_runs = 5000 libs = ["lib"] remappings = ["@openzeppelin/=lib/openzeppelin-contracts/", "@aave/=lib/aave-v3-core/"]
CI (
).github/workflows/ci.yml
forge fmt --check, forge build, forge test -vvv, forge coverage, slither . (if available)Pre‑commit
solhint, prettier-plugin-solidity, forge fmtUse Foundry. Include unit, scenario, and property/invariant tests.
Core_Quote.t.sol
p30 = baseU → U_max-ε → p30 ≤ p30MaxPool_Buy.t.sol
cFree >= lockAmt and U_new < U_maxU_new ≥ U_max (capacity exceeded)activationDelay would cross maturitytreasury receives fee, cTotal += poolTake, cFree -= lockAmt + ...Activation_Gating.t.sol
redeem in settled series rejects CT with activationStart > eventTimeactivationStart <= eventTimePool_Settle.t.sol
payoutRatio=1e18 pays all lockedr*N, returns leftover to cFreepaused=true, cooldownUntil setPool_Mature.t.sol
cLocked → cFree, L_total decremented; cTotal unchangedLP_DepositWithdraw.t.sol
shares=amountshares = amount*S_total/NAVcFree), queues remainder, burns shares proportionallyRBC_Guards.t.sol
requiredCapital viewcTotal < required, sales are blocked (or Pool is paused)Pause_Cooldown.t.sol
pause() blocks buybuy blocked, deposit allowed, redeem allowedDecimals_6_18.t.sol
/test/property/Invariants.t.sol)Express as always‑true properties after each action:
∀j: C_locked[j] >= rMax[j]*N_j (by design equal; test equality)L_total == Σ_j C_locked[j]C_total == C_free + Σ_j C_locked[j]buy: U_new < U_maxwithdrawable ≤ C_freepost‑settle: paused == true and cooldownUntil > nowpayTotalseries.settled or matured, cannot transition back to activeUse fuzzing: vary
notional, tenor, times, decimals, fee rates, k, S*. Include corner cases: U≈0, U≈U_max, p30≈p30Max, tiny notionals, huge notionals, fee=0, fee>0.
Deploy.s.sol
Deploy CT and LP tokens
Initialize
PoolStorage.ProductConfig:
Set initial RBC params (optional)
Grant roles: GOVERNOR to multisig, OPERATOR to ops wallet, POOL role to Pool in tokens
CreateSeries.s.sol
seriesId = keccak(productId, maturity) via TokenIdLib, call PoolAdmin.createSeries(maturity, rMaxWad)Seed.s.sol
cFree via depositRunbook
Pool.settle(seriesId)settleQueue() periodically or anyone can call and take a keeper tip if we add incentives (not in MVP)buy must check both paused and cooldownUntil.eventTime != 0, and reject multiple settlements. Prevent oracle from returning r > rMax.redeem pay computation). Prefer to pass through Core results when possible.pre/post balances if needed.PoolStorage.Layout storage L = PoolStorage.layout();)uint64 times together)unchecked in tight increments only when proven safememory over calldata only where neededThe build is “done” when:
All unit and scenario tests pass with ≥ 95% coverage
All invariants in property tests hold under fuzzing (≥ 100k runs per seed in CI)
forge script deployments succeed on a local fork with:
Slither reports no high/medium issues (false positives documented)
Gas snapshots recorded; hot paths (
buy, redeem, deposit, withdraw) are within budget (< 350k each in typical flows for 6‑dec asset on L2; document actuals)
Sprint 1
Sprint 2
buy), deposit, withdraw (without queue)Sprint 3
settle), maturity (mature), redeemSprint 4
Sprint 5
Update
README.md with:
P_T (replace linear with 1 - (1 - P30)^(T/30) using ln/exp approximation with safe bounds)If you implement exactly the above, you will have a production‑grade MVP that respects the invariants (purchase‑time lock, capacity guard, free‑capital withdrawals) and is auditable with clear separation between pure Core and imperative orchestration.