Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/*──────────────────────────────────────────────────────────────────────────────
│ ERC1967Proxy - Transparent Upgradeable Proxy (minimal, hardened)
│
│ IMPORTANT OPERATIONAL GUIDELINES
│ - ALWAYS pass the encoded initializer (`_data`) for the implementation when
│ deploying the proxy. This version enforces it at runtime.
│ - The proxy "admin" is the upgrade authority ONLY. Admin cannot use app
│ functions through the proxy (by design). Use a separate EOA/account for UX.
│ - Recommended: make the admin a multisig.
│
│ Pattern: Transparent Proxy (EIP-1967 storage slots)
│ The implementation contract (e.g., OracleRNG) should expose an `initialize()`
│ function instead of a constructor so state can be set via `_data`.
│
│ This proxy restricts `fallback` when `msg.sender == admin` to prevent the
│ admin from accidentally interacting with the app.
└──────────────────────────────────────────────────────────────────────────────*/
contract ERC1967Proxy {
// EIP-1967 slots (implementation = keccak256("eip1967.proxy.implementation")-1, admin likewise)
bytes32 private constant IMPLEMENTATION_SLOT =
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
bytes32 private constant ADMIN_SLOT =
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
event Upgraded(address indexed implementation);
event AdminChanged(address indexed previousAdmin, address indexed newAdmin);
/**
* @param _logic The address of the initial implementation
* @param _admin The admin (upgrade authority). Use a multisig!
* @param _data Call data to initialize the implementation's storage
*/
constructor(address _logic, address _admin, bytes memory _data) payable {
require(_isContract(_logic), "ERC1967Proxy: logic not contract");
require(_admin != address(0), "ERC1967Proxy: zero admin");
require(_data.length > 0, "ERC1967Proxy: initializer required");
_setAdmin(_admin);
_setImplementation(_logic);
// delegatecall initializer into implementation
(bool success, bytes memory returndata) = _logic.delegatecall(_data);
require(success, string(returndata));
}
modifier ifAdmin() {
if (msg.sender == _getAdmin()) {
_;
} else {
_fallback();
}
}
/* ───────────────────────── Admin Interface (only as admin) ───────────────────────── */
function upgradeTo(address newImplementation) external ifAdmin {
require(_isContract(newImplementation), "ERC1967Proxy: not contract");
_setImplementation(newImplementation);
emit Upgraded(newImplementation);
}
function upgradeToAndCall(address newImplementation, bytes calldata data)
external
payable
ifAdmin
{
require(_isContract(newImplementation), "ERC1967Proxy: not contract");
_setImplementation(newImplementation);
emit Upgraded(newImplementation);
(bool success, bytes memory returndata) = newImplementation.delegatecall(data);
require(success, string(returndata));
}
function changeAdmin(address newAdmin) external ifAdmin {
require(newAdmin != address(0), "ERC1967Proxy: zero admin");
address oldAdmin = _getAdmin();
_setAdmin(newAdmin);
emit AdminChanged(oldAdmin, newAdmin);
}
/// @notice Viewable only by admin to avoid accidental UX misuse
function admin() external ifAdmin returns (address) {
return _getAdmin();
}
/// @notice Viewable only by admin to avoid accidental UX misuse
function implementation() external ifAdmin returns (address) {
return _getImplementation();
}
/* ────────────────────────────── Fallback/Receive ────────────────────────────── */
fallback() external payable {
_fallback();
}
receive() external payable {
_fallback();
}
function _fallback() internal {
// Admin is blocked from application calls through the proxy by design
require(msg.sender != _getAdmin(), "ERC1967Proxy: admin cannot fallback");
_delegate(_getImplementation());
}
function _delegate(address impl) internal {
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
/* ───────────────────────────── Internal slot helpers ───────────────────────────── */
function _getAdmin() private view returns (address adm) {
bytes32 slot = ADMIN_SLOT;
assembly { adm := sload(slot) }
}
function _setAdmin(address newAdmin) private {
bytes32 slot = ADMIN_SLOT;
assembly { sstore(slot, newAdmin) }
}
function _getImplementation() private view returns (address impl) {
bytes32 slot = IMPLEMENTATION_SLOT;
assembly { impl := sload(slot) }
}
function _setImplementation(address newImplementation) private {
bytes32 slot = IMPLEMENTATION_SLOT;
assembly { sstore(slot, newImplementation) }
}
function _isContract(address account) private view returns (bool) {
return account.code.length > 0;
}
}
/*──────────────────────────────────────────────────────────────────────────────
│ IOracleRNGConsumer - Callback interface for RNG consumers
└──────────────────────────────────────────────────────────────────────────────*/
interface IOracleRNGConsumer {
function fulfillRandomness(uint256 requestId, uint256 randomness) external;
}
/*──────────────────────────────────────────────────────────────────────────────
│ OracleRNG - Minimal Randomness Oracle + PvP example (Upgradeable via Proxy)
│
│ SECURITY NOTE
│ - Uses blockhash-based entropy. Good for low-stakes; NOT for high-stakes.
│ - For valuable decisions, prefer VRF or commit-reveal (upgradeable later).
│
│ PATCHES IMPLEMENTED (relative to the original):
│ 1) RNG finalization can proceed with ZERO participants (no rewards then).
│ 2) Finalization requires block.number > deadline (off-by-one fixed).
│ 3) Added owner() getter; initialize() must be called exactly once via proxy.
│ 4) requestRngWhitelisted validates array size before casting to uint16.
│ 5) PvP auto-joins P1/P2 into the RNG (resilience) without exceeding caps.
│ 6) Escape hatches: cancelOpenBattle() and cancelStuckBattle() to avoid
│ stranded escrow (expired RNG or never-joined scenarios).
│ 7) Enforced non-zero owner in initialize() to avoid proxy-bricking.
│ 8) Minor gas polish in loops + storage gap for safer upgrades.
└──────────────────────────────────────────────────────────────────────────────*/
contract OracleRNG {
/* ─────────────── ERC20 Minimal ─────────────── */
string public name;
string public symbol;
uint8 public decimals;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
/* ───────────── Ownership & Reentrancy ───────────── */
address private _owner;
bool private _initialized;
uint256 private _entered; // 0 = not entered, 1 = entered
modifier onlyOwner() {
require(msg.sender == _owner, "Not owner");
_;
}
modifier nonReentrant() {
require(_entered == 0, "Reentrancy");
_entered = 1;
_;
_entered = 0;
}
event OwnershipTransferred(address indexed from, address indexed to);
/* ───────────── Oracle State ───────────── */
uint256 public oracleFee; // fee in wei per RNG request
uint256 public nextRngId;
uint256 public nextBattleId;
struct RngRequest {
address requester;
uint64 deadline; // block number after which finalization can start
uint64 expiryBlock; // block number after which finalization MUST fail (blockhash window)
uint16 maxParticipants; // cap to prevent unbounded gas in finalize
uint16 participantCount; // number of unique participants
uint256 randomResult; // final randomness (0 until finalized)
address[] participants; // list for reward splitting
bool isWhitelisted;
bool callbackOnFinalize;
bool finalized;
mapping(address => bool) whitelist; // only used when isWhitelisted == true
}
mapping(uint256 => RngRequest) private _rngRequests;
mapping(uint256 => mapping(address => bool)) public hasJoined;
/* ───────────── PvP State ───────────── */
enum BattleState { None, WaitingP2, WaitingRng, Resolved }
struct Battle {
address player1;
address player2;
uint256 wager; // tokens escrowed (oRNG)
uint256 rngId; // RNG used to decide winner
BattleState state;
}
mapping(uint256 => Battle) private _battles;
mapping(uint256 => uint256) private _battleEscrow;
/* ───────────── Events ───────────── */
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
event RngRequested(uint256 indexed id, address indexed requester);
event RngJoined(uint256 indexed id, address indexed participant);
event RngFinalized(uint256 indexed id, uint256 randomness);
event BattleCreated(uint256 indexed id, address indexed player1, uint256 wager);
event BattleMatched(uint256 indexed id, address indexed player2);
event BattleResolved(uint256 indexed id, address indexed winner, uint256 prize);
event OracleFeeUpdated(uint256 oldFee, uint256 newFee);
event FeeWithdrawn(address indexed to, uint256 amount);
/* ───────────── Initialization ───────────── */
/**
* @notice Must be invoked via proxy constructor `_data` on deployment.
* @param owner_ Owner address (recommended: multisig). MUST be non-zero.
* @param fee Fee per RNG request in wei (use 0.001 ether if zero provided).
*/
function initialize(address owner_, uint256 fee) external {
require(!_initialized, "Initialized");
require(owner_ != address(0), "Owner required");
_initialized = true;
_owner = owner_;
emit OwnershipTransferred(address(0), _owner);
name = "Oracle RNG";
symbol = "oRNG";
decimals = 18;
oracleFee = fee == 0 ? 0.001 ether : fee;
nextRngId = 1;
nextBattleId = 1;
}
/// @notice Standard getter (tooling/UI expect this to exist)
function owner() public view returns (address) {
return _owner;
}
/**
* @notice Transfer ownership. Use a multisig to minimize risk.
*/
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "Zero owner");
emit OwnershipTransferred(_owner, newOwner);
_owner = newOwner;
}
/* ───────────── Minimal ERC20 ───────────── */
function transfer(address to, uint256 amount) external returns (bool) {
_transfer(msg.sender, to, amount);
return true;
}
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
uint256 allowed = allowance[from][msg.sender];
if (allowed != type(uint256).max) {
allowance[from][msg.sender] = allowed - amount; // will revert if insufficient
}
_transfer(from, to, amount);
return true;
}
function _transfer(address from, address to, uint256 amount) internal {
require(to != address(0), "ERC20: transfer to zero");
balanceOf[from] -= amount; // reverts if insufficient
balanceOf[to] += amount;
emit Transfer(from, to, amount);
}
function _mint(address to, uint256 amount) internal {
require(to != address(0), "ERC20: mint to zero");
totalSupply += amount;
balanceOf[to] += amount;
emit Transfer(address(0), to, amount);
}
function _burn(address from, uint256 amount) internal {
balanceOf[from] -= amount; // reverts if insufficient
totalSupply -= amount;
emit Transfer(from, address(0), amount);
}
/* ───────────── RNG CORE ───────────── */
/**
* @notice Request RNG with open participation and app callback on finalize.
* @dev Requires `msg.value >= oracleFee`. Excess is retained (deliberate).
*/
function requestRng(uint64 waitBlocks, uint16 maxParticipants)
external
payable
returns (uint256 id)
{
require(msg.value >= oracleFee, "Fee");
id = _createRng(msg.sender, waitBlocks, maxParticipants, false, new address[](0), true);
}
/**
* @notice Request RNG without callback to requester on finalize.
*/
function requestRngNoCallback(uint64 waitBlocks, uint16 maxParticipants)
external
payable
returns (uint256 id)
{
require(msg.value >= oracleFee, "Fee");
id = _createRng(msg.sender, waitBlocks, maxParticipants, false, new address[](0), false);
}
/**
* @notice Request RNG with a fixed whitelist (participants must exist in `wl`).
*/
function requestRngWhitelisted(uint64 waitBlocks, address[] calldata wl)
external
payable
returns (uint256 id)
{
require(msg.value >= oracleFee, "Fee");
require(wl.length > 0 && wl.length <= 100, "wl size");
id = _createRng(msg.sender, waitBlocks, uint16(wl.length), true, wl, true);
}
function _createRng(
address requester,
uint64 waitBlocks,
uint16 maxParticipants,
bool isWhitelisted,
address[] memory wl,
bool cb
) internal returns (uint256 id) {
require(waitBlocks >= 10 && waitBlocks <= 200, "wait");
require(maxParticipants > 0 && maxParticipants <= 100, "max");
id = nextRngId++;
RngRequest storage r = _rngRequests[id];
r.requester = requester;
r.deadline = uint64(block.number + waitBlocks);
// expiryBlock is set to comfortably within the 256-block hash window
r.expiryBlock = uint64(block.number + waitBlocks + 255);
r.maxParticipants = maxParticipants;
r.isWhitelisted = isWhitelisted;
r.callbackOnFinalize = cb;
if (isWhitelisted) {
for (uint256 i = 0; i < wl.length; ) {
address addr = wl[i];
require(addr != address(0), "wl");
r.whitelist[addr] = true;
unchecked { ++i; }
}
}
emit RngRequested(id, requester);
}
/**
* @notice Join an RNG as a participant to be mixed into entropy and share rewards.
*/
function joinRng(uint256 id) external {
RngRequest storage r = _rngRequests[id];
require(r.deadline != 0, "no rng");
require(!r.finalized, "done");
require(block.number < r.deadline, "deadline");
require(!hasJoined[id][msg.sender], "joined");
require(r.participantCount < r.maxParticipants, "full");
if (r.isWhitelisted) require(r.whitelist[msg.sender], "not wl");
hasJoined[id][msg.sender] = true;
r.participants.push(msg.sender);
r.participantCount++;
emit RngJoined(id, msg.sender);
}
/**
* @notice Finalize RNG: computes randomness using blockhash(deadline) optionally
* mixed with participants. Can succeed with ZERO participants (no rewards).
* @dev Off-by-one fixed: requires `block.number > deadline` (not >=).
*/
function finalizeRng(uint256 id) public nonReentrant {
RngRequest storage r = _rngRequests[id];
require(!r.finalized, "finalized");
require(block.number > r.deadline, "early"); // fixed: strictly greater than deadline
require(block.number < r.expiryBlock, "expired");
bytes32 e = blockhash(r.deadline);
require(e != 0, "bh"); // ensure deadline is within the last 256 blocks
// Mix participant addresses into entropy only if there are any
uint256 pc = r.participantCount;
if (pc > 0) {
address[] storage parts = r.participants;
for (uint256 i = 0; i < pc; ) {
e = keccak256(abi.encodePacked(e, parts[i]));
unchecked { ++i; }
}
// Reward: 1 token split among participants (exact-sum split)
uint256 base = 1e18 / pc;
uint256 total;
for (uint256 i = 0; i + 1 < pc; ) {
_mint(parts[i], base);
total += base;
unchecked { ++i; }
}
_mint(parts[pc - 1], 1e18 - total);
}
uint256 randomness = uint256(e);
r.randomResult = randomness;
r.finalized = true;
emit RngFinalized(id, randomness);
if (r.callbackOnFinalize && _isContract(r.requester)) {
// Best-effort callback; ignore failures
try IOracleRNGConsumer(r.requester).fulfillRandomness(id, randomness) {} catch {}
}
}
/* ───────────── Views to aid ops/UX ───────────── */
function getRngParticipants(uint256 id) external view returns (address[] memory) {
return _rngRequests[id].participants;
}
function getRngMeta(uint256 id)
external
view
returns (
address requester,
uint64 deadline,
uint64 expiryBlock,
uint16 maxParticipants,
uint16 participantCount,
bool isWhitelisted,
bool callbackOnFinalize,
bool finalized,
uint256 randomResult
)
{
RngRequest storage r = _rngRequests[id];
return (
r.requester,
r.deadline,
r.expiryBlock,
r.maxParticipants,
r.participantCount,
r.isWhitelisted,
r.callbackOnFinalize,
r.finalized,
r.randomResult
);
}
/* ───────────── PvP (Player vs Player) ─────────────
Operational guidance:
- Pass `maxParticipants >= 2` if you rely on auto-joining both players.
- Battles auto-join P1 & P2 to the RNG (best effort); if full or WL
prevents joining, the zero-participant finalization patch prevents lockups.
- Escape hatches are provided to unwind if RNG expires or battle never matches.
────────────────────────────────────────────────────*/
function _joinRngInternal(uint256 id, address p) internal {
RngRequest storage r = _rngRequests[id];
if (r.finalized) return;
if (block.number >= r.deadline) return; // ← prevent post‑deadline joins
if (hasJoined[id][p]) return;
if (r.participantCount >= r.maxParticipants) return;
if (r.isWhitelisted && !r.whitelist[p]) return;
hasJoined[id][p] = true;
r.participants.push(p);
r.participantCount++;
emit RngJoined(id, p);
}
function createBattle(uint256 wager, uint64 waitBlocks, uint16 maxParticipants)
external
returns (uint256 bid)
{
require(wager > 0, "wager");
require(balanceOf[msg.sender] >= wager, "bal");
_burn(msg.sender, wager);
// Create RNG owned by this contract; no callback needed for PvP
uint256 rngId = _createRng(address(this), waitBlocks, maxParticipants, false, new address[](0), false);
// Auto-join P1 (best effort; caps & WL are respected)
_joinRngInternal(rngId, msg.sender);
bid = nextBattleId++;
Battle storage b = _battles[bid];
b.player1 = msg.sender;
b.wager = wager;
b.rngId = rngId;
b.state = BattleState.WaitingP2;
_battleEscrow[bid] = wager;
emit BattleCreated(bid, msg.sender, wager);
}
function matchBattle(uint256 bid) external {
Battle storage b = _battles[bid];
require(b.state == BattleState.WaitingP2, "state");
require(b.player1 != msg.sender, "self");
RngRequest storage r = _rngRequests[b.rngId];
require(block.number < r.deadline, "rng closed"); // ← ensure RNG still open
require(balanceOf[msg.sender] >= b.wager, "bal");
_burn(msg.sender, b.wager);
b.player2 = msg.sender;
b.state = BattleState.WaitingRng;
_battleEscrow[bid] += b.wager;
emit BattleMatched(bid, msg.sender);
_joinRngInternal(b.rngId, msg.sender);
}
/**
* @notice Resolve a matched battle. Will try to finalize RNG on-demand.
* @dev Uses an external call to `finalizeRng` to respect the nonReentrant guard.
*/
function resolveBattle(uint256 bid) external {
Battle storage b = _battles[bid];
require(b.state == BattleState.WaitingRng, "state");
RngRequest storage r = _rngRequests[b.rngId];
// If possible, auto-finalize on demand to reduce UX friction.
if (!r.finalized && block.number > r.deadline && block.number < r.expiryBlock) {
// external call to observe nonReentrant guard within finalizeRng
this.finalizeRng(b.rngId);
r = _rngRequests[b.rngId]; // refresh storage ref
}
require(r.finalized, "rng");
// Winner by LSB of randomness
address winner = (r.randomResult & 1) == 0 ? b.player1 : b.player2;
uint256 esc = _battleEscrow[bid];
b.state = BattleState.Resolved;
delete _battleEscrow[bid];
_mint(winner, esc);
emit BattleResolved(bid, winner, esc);
}
/**
* @notice Cancel an open battle (no P2 yet). Returns escrow to P1.
*/
function cancelOpenBattle(uint256 bid) external {
Battle storage b = _battles[bid];
require(b.state == BattleState.WaitingP2, "state");
require(msg.sender == b.player1 || msg.sender == _owner, "auth");
uint256 esc = _battleEscrow[bid];
b.state = BattleState.Resolved;
delete _battleEscrow[bid];
_mint(b.player1, esc);
emit BattleResolved(bid, b.player1, esc);
}
/**
* @notice Cancel a stuck battle if RNG expired (missed blockhash window)
* OR no participants joined and deadline passed.
* @dev If P2 has joined, escrow is split back evenly; otherwise all to P1.
*/
function cancelStuckBattle(uint256 bid) external {
Battle storage b = _battles[bid];
require(b.state == BattleState.WaitingRng, "state");
require(msg.sender == b.player1 || msg.sender == b.player2 || msg.sender == _owner, "auth");
RngRequest storage r = _rngRequests[b.rngId];
bool rngExpired = block.number >= r.expiryBlock;
bool noParticipantsAndPastDeadline = (block.number > r.deadline && r.participantCount == 0);
require(rngExpired || noParticipantsAndPastDeadline, "not cancellable");
uint256 esc = _battleEscrow[bid];
b.state = BattleState.Resolved;
delete _battleEscrow[bid];
if (b.player2 == address(0)) {
// Never matched
_mint(b.player1, esc);
emit BattleResolved(bid, b.player1, esc);
} else {
// Matched but stuck; refund fairly
uint256 half = esc / 2;
_mint(b.player1, half);
_mint(b.player2, esc - half);
emit BattleResolved(bid, address(0), esc); // zero-address signals cancel/refund
}
}
/* ───────────── Admin Ops ───────────── */
function setOracleFee(uint256 newFee) external onlyOwner {
uint256 old = oracleFee;
oracleFee = newFee;
emit OracleFeeUpdated(old, newFee);
}
/**
* @notice Withdraw accumulated request fees (ETH).
* @dev NonReentrant to avoid reentrancy via receiver hooks.
*/
function withdrawFees(address payable to) external onlyOwner nonReentrant {
uint256 amt = address(this).balance;
(bool ok, ) = to.call{value: amt}("");
require(ok, "transfer");
emit FeeWithdrawn(to, amt);
}
/* ───────────── Helpers ───────────── */
function _isContract(address a) internal view returns (bool) {
return a.code.length > 0;
}
/* ───────────── Storage gap for future upgrades ───────────── */
uint256[50] private __gap;
}
/*──────────────────────────────────────────────────────────────────────────────
│ DEPLOYMENT GUIDE
│
│ // 1) Deploy implementation
│ const Oracle = await ethers.getContractFactory("OracleRNG");
│ const impl = await Oracle.deploy();
│ await impl.deployed();
│
│ // 2) Encode initializer (use a MULTISIG as owner; must be non-zero!)
│ // ethers v6:
│ const owner = "<YOUR_MULTISIG>";
│ const fee = ethers.parseEther("0.001"); // or your chosen default
│ const initData = impl.interface.encodeFunctionData("initialize", [owner, fee]);
│
│ // 3) Deploy proxy; admin is UPGRADE AUTHORITY ONLY (ideally a multisig)
│ const Proxy = await ethers.getContractFactory("ERC1967Proxy");
│ const admin = "<YOUR_PROXY_ADMIN_MULTISIG>";
│ const proxy = await Proxy.deploy(impl.target, admin, initData); // ethers v6 uses .target
│ await proxy.deployed();
│
│ // 4) Interact with the proxied instance
│ const rng = Oracle.attach(proxy.target);
│
│ OPERATIONAL NOTES
│ - Proxy constructor now REQUIRES non-empty initializer calldata.
│ - OracleRNG.initialize REQUIRES non-zero owner (prevents proxy-bricking).
│ - Admin cannot call application functions via the proxy; use a separate EOA.
│ - For PvP, choose `maxParticipants >= 2` to ensure both players can auto-join.
│ - If the RNG deadline passes without participants, finalize still works (no rewards).
│ - If the RNG expires (missed blockhash window), use `cancelStuckBattle()`.
│ - This RNG is NOT cryptographically secure. Upgrade to VRF for high-stakes.
└──────────────────────────────────────────────────────────────────────────────*/
Submitted on: 2025-10-17 10:44:16
Comments
Log in to comment.
No comments yet.