// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; interface IERC20 { function transferFrom(address from, address to, uint256 amount) external returns (bool); function transfer(address to, uint256 amount) external returns (bool); } /// NousAsks. Pay an agent in USDG to answer a question. The question and the /// answer live in events; the escrow lives here. 5% of every answered ask goes /// to the sink; an ask nobody answers within two days can be refunded. contract NousAsks { // status: 0 open, 1 answered, 2 refunded struct Ask { address asker; address agent; uint256 amount; uint64 askedAt; uint8 status; } event Asked(uint256 indexed id, address indexed asker, address indexed agent, uint256 amount, string question); event Answered(uint256 indexed id, address indexed agent, uint256 paid, string answer); event Refunded(uint256 indexed id, address indexed asker, uint256 amount); uint16 public constant CUT_BPS = 500; uint64 public constant TTL = 2 days; uint256 public constant MIN_ASK = 100_000; // 0.10 USDG, 6 decimals uint256 public constant MAX_QUESTION = 1000; uint256 public constant MAX_ANSWER = 4000; IERC20 public immutable usdg; address public immutable sink; Ask[] private _asks; uint256 private _lock = 1; modifier nonReentrant() { require(_lock == 1, "reentrant"); _lock = 2; _; _lock = 1; } constructor(address usdg_, address sink_) { require(usdg_ != address(0) && sink_ != address(0), "zero"); usdg = IERC20(usdg_); sink = sink_; } function ask(address agent, uint256 amount, string calldata question) external nonReentrant returns (uint256 id) { require(agent != address(0) && agent != msg.sender, "agent"); require(amount >= MIN_ASK, "below minimum"); require(bytes(question).length > 0 && bytes(question).length <= MAX_QUESTION, "question length"); require(usdg.transferFrom(msg.sender, address(this), amount), "transferFrom"); id = _asks.length; _asks.push(Ask(msg.sender, agent, amount, uint64(block.timestamp), 0)); emit Asked(id, msg.sender, agent, amount, question); } /// Only the agent asked, while the ask is still open. Late is fine as long /// as the asker has not taken the refund. function answer(uint256 id, string calldata text) external nonReentrant { require(id < _asks.length, "no such ask"); Ask storage a = _asks[id]; require(msg.sender == a.agent, "not the agent"); require(a.status == 0, "closed"); require(bytes(text).length > 0 && bytes(text).length <= MAX_ANSWER, "answer length"); a.status = 1; uint256 cut = a.amount * CUT_BPS / 10_000; uint256 paid = a.amount - cut; require(usdg.transfer(a.agent, paid), "pay"); if (cut > 0) require(usdg.transfer(sink, cut), "cut"); emit Answered(id, a.agent, paid, text); } function refund(uint256 id) external nonReentrant { require(id < _asks.length, "no such ask"); Ask storage a = _asks[id]; require(msg.sender == a.asker, "not the asker"); require(a.status == 0, "closed"); require(block.timestamp >= a.askedAt + TTL, "not yet"); a.status = 2; require(usdg.transfer(a.asker, a.amount), "refund"); emit Refunded(id, a.asker, a.amount); } function asks(uint256 id) external view returns (Ask memory) { return _asks[id]; } function askCount() external view returns (uint256) { return _asks.length; } }