← Index
Source: docs/archive/nostra/designs/task-2-deposit-withdrawal.md (auto-generated by scripts/generate-docs-html.mjs — edit the .md, not this file)

Task 2: Deposit/Withdrawal Model Design

Objective

Implement a Deposit/Withdrawal system where users deposit USDC into the CTFExchange contract. Trading will then settle using these internal balances instead of performing ERC20 transfers for every trade.

Current State

Proposed Solution

1. Smart Contract Changes (nostra-contracts)

New Mixin: contracts/exchange/mixins/Balances.sol

abstract contract Balances {
    // Mapping of user address to collateral balance
    mapping(address => uint256) public balances;

    event Deposit(address indexed user, uint256 amount);
    event Withdraw(address indexed user, uint256 amount);

    function deposit(uint256 amount) external virtual;
    function withdraw(uint256 amount) external virtual;
}

Modify CTFExchange.sol:

Modify AssetOperations.sol:

Migration/Deployment:

2. Frontend Implementation (web)

UI Components:

Integration:

Security Considerations (CRITICAL)

Step-by-Step Plan

  1. Contracts: Create Balances mixin with security modifiers.
  2. Contracts: Update CTFExchange to inherit Balances.
  3. Contracts: Update AssetOperations to use internal accounting for Collateral.
  4. Contracts: Write tests for Deposit/Withdraw/Trade flow.
  5. Frontend: Implement Deposit/Withdraw UI.

Addendum: Market Resolution Fix (2024-12-03)

Bug: "Condition not found" Error During Market Resolution

Symptom: When resolving markets via /api/admin/resolve, the transaction fails with "Condition not found".

Root Cause: The admin.ts was calling conditionalTokens.reportPayouts() directly from the server wallet. However, reportPayouts uses msg.sender to derive the conditionId:

// In ConditionalTokens.reportPayouts():
bytes32 conditionId = getConditionId(msg.sender, questionId, outcomeSlotCount);

When the market was created via MarketFactory.createBinaryMarket(), the condition was prepared with ResolutionOracle as the oracle:

// In MarketFactory.createBinaryMarket():
ctf.prepareCondition(oracle, questionId, 2);  // oracle = ResolutionOracle address

So the stored conditionId = keccak256(ResolutionOracle, questionId, 2).

But when the server wallet called reportPayouts directly, it looked up: keccak256(serverWallet, questionId, 2) - which doesn't exist!

The Fix

Files Changed:

  1. api/src/config/blockchain.ts - Added resolutionOracleAddress to config
  2. api/src/routes/admin.ts - Changed to use ResolutionOracle
  3. api/src/abis/ResolutionOracle.json - Added ABI file (copied from nostra-contracts)

Code Change (admin.ts):

// BEFORE (wrong):
const tx = await conditionalTokens.reportPayouts(questionId, payouts);

// AFTER (correct):
const tx = await resolutionOracle.adminFinalizeResolution(
  questionId,
  outcomeSlotCount,  // 2 for binary markets
  payouts
);

Why This Works:


Addendum: Redemption Payout Fix (2024-12-03)

Bug: Payout Goes to Wallet Instead of Exchange Balance

Symptom: After claiming winnings, Portfolio shows incorrect value. User deposited $1000, won $9.23, but Portfolio shows $990 (the remaining exchange balance) instead of $1009.23.

Root Cause: The ConditionalTokens.redeemPositions() function sends the USDC payout directly to msg.sender's wallet:

// In ConditionalTokens.redeemPositions() - line 187:
require(collateralToken.transfer(msg.sender, totalPayout), "Transfer failed");

This bypasses the deposit/withdrawal model. The payout goes to the user's wallet, but the frontend displays the exchange balance (from balances[user]), not the wallet balance.

The Fix

File Changed: web/src/app/my-positions/page.tsx

Code Change (handleClaim function):

// After redeemPositions tx.wait(), auto-deposit payout into exchange:

const payoutAmount = data.expectedPayout;
if (payoutAmount && payoutAmount > 0) {
    // Fetch contract addresses
    const configResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/config/contracts`);
    const configData = await configResponse.json();
    const exchangeAddress = configData.contracts.CTFExchange;
    const usdcAddress = configData.contracts.MockUSDC;

    // Approve and deposit
    const usdc = new ethers.Contract(usdcAddress, [...], signer);
    const exchange = new ethers.Contract(exchangeAddress, [...], signer);
    const payoutWei = ethers.parseUnits(payoutAmount.toString(), 6);

    await usdc.approve(exchangeAddress, payoutWei);
    await exchange.deposit(payoutWei);
}

Why This Works:

Withdraw Verification

The withdraw function in DepositWithdrawModal.tsx correctly transfers all funds to wallet:

const exchange = new ethers.Contract(exchangeAddress, [
    "function withdraw(uint256 amount)"
], signer);
const amountWei = ethers.parseUnits(amount, 6);
await exchange.withdraw(amountWei);

This calls Balances._withdraw() which:

  1. Checks balances[user] >= amount
  2. Deducts from balances[user]
  3. Transfers USDC to user via safeTransfer