Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 1 addition & 12 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
"./deploymentAddresses": {
"types": "./ts/deploymentAddresses.ts",
"default": "./js/deploymentAddresses.js"
},
"./protocolConfig": {
"types": "./ts/protocolConfig.ts",
"default": "./js/protocolConfig.js"
}
},
"files": [
Expand Down
100 changes: 100 additions & 0 deletions shared/ts/protocolConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
export type ProtocolConfig = {
forkBurnDivisor: bigint
forkThresholdDivisor: bigint
initialEscalationGameDeposit: bigint
}

export type ProtocolConfigInput = Partial<{
[key in keyof ProtocolConfig]: bigint | number | string | undefined
}>

export const DEFAULT_FORK_THRESHOLD_DIVISOR = 20n
export const DEFAULT_FORK_BURN_DIVISOR = 5n
export const DEFAULT_INITIAL_ESCALATION_GAME_DEPOSIT = 10n ** 18n

export const DEFAULT_PROTOCOL_CONFIG: ProtocolConfig = {
forkBurnDivisor: DEFAULT_FORK_BURN_DIVISOR,
forkThresholdDivisor: DEFAULT_FORK_THRESHOLD_DIVISOR,
initialEscalationGameDeposit: DEFAULT_INITIAL_ESCALATION_GAME_DEPOSIT,
}

const PROTOCOL_CONFIG_GLOBAL_KEY = '__ZOLTAR_PROTOCOL_CONFIG__'

function parseConfigBigInt(value: bigint | number | string | undefined, field: keyof ProtocolConfig): bigint | undefined {
if (value === undefined) return undefined
if (typeof value === 'bigint') return value
if (typeof value === 'number') {
if (!Number.isInteger(value)) throw new Error(`Protocol config ${field} must be an integer`)
return BigInt(value)
}
const trimmedValue = value.trim()
if (trimmedValue === '') return undefined
return BigInt(trimmedValue)
}

function readProcessEnv(name: string): string | undefined {
const processValue = Reflect.get(globalThis, 'process')
if (typeof processValue !== 'object' || processValue === null) return undefined
const envValue = Reflect.get(processValue, 'env')
if (typeof envValue !== 'object' || envValue === null) return undefined
const rawValue = Reflect.get(envValue, name)
if (typeof rawValue !== 'string') return undefined
const trimmedValue = rawValue.trim()
return trimmedValue === '' ? undefined : trimmedValue
}

function getEnvironmentProtocolConfigOverrides(): ProtocolConfigInput {
const forkBurnDivisor = readProcessEnv('ZOLTAR_FORK_BURN_DIVISOR')
const forkThresholdDivisor = readProcessEnv('ZOLTAR_FORK_THRESHOLD_DIVISOR')
const initialEscalationGameDeposit = readProcessEnv('ZOLTAR_INITIAL_ESCALATION_GAME_DEPOSIT')
return {
...(forkBurnDivisor === undefined ? {} : { forkBurnDivisor }),
...(forkThresholdDivisor === undefined ? {} : { forkThresholdDivisor }),
...(initialEscalationGameDeposit === undefined ? {} : { initialEscalationGameDeposit }),
}
}

function readProtocolConfigOverrideValue(source: object, field: keyof ProtocolConfig) {
const rawValue = Reflect.get(source, field)
if (typeof rawValue === 'bigint' || typeof rawValue === 'number' || typeof rawValue === 'string') return rawValue
return undefined
}

function getGlobalProtocolConfigOverrides(): ProtocolConfigInput {
const rawConfig = Reflect.get(globalThis, PROTOCOL_CONFIG_GLOBAL_KEY)
if (typeof rawConfig !== 'object' || rawConfig === null) return {}
const forkBurnDivisor = readProtocolConfigOverrideValue(rawConfig, 'forkBurnDivisor')
const forkThresholdDivisor = readProtocolConfigOverrideValue(rawConfig, 'forkThresholdDivisor')
const initialEscalationGameDeposit = readProtocolConfigOverrideValue(rawConfig, 'initialEscalationGameDeposit')
return {
...(forkBurnDivisor === undefined ? {} : { forkBurnDivisor }),
...(forkThresholdDivisor === undefined ? {} : { forkThresholdDivisor }),
...(initialEscalationGameDeposit === undefined ? {} : { initialEscalationGameDeposit }),
}
}

export function validateProtocolConfig(config: ProtocolConfigInput): ProtocolConfig {
const forkBurnDivisor = parseConfigBigInt(config.forkBurnDivisor, 'forkBurnDivisor')
const forkThresholdDivisor = parseConfigBigInt(config.forkThresholdDivisor, 'forkThresholdDivisor')
const initialEscalationGameDeposit = parseConfigBigInt(config.initialEscalationGameDeposit, 'initialEscalationGameDeposit')
if (forkThresholdDivisor === undefined) throw new Error('Protocol config forkThresholdDivisor is required')
if (forkBurnDivisor === undefined) throw new Error('Protocol config forkBurnDivisor is required')
if (initialEscalationGameDeposit === undefined) throw new Error('Protocol config initialEscalationGameDeposit is required')
if (forkThresholdDivisor <= 1n) throw new Error('Protocol config forkThresholdDivisor must be greater than 1')
if (forkBurnDivisor <= 1n) throw new Error('Protocol config forkBurnDivisor must be greater than 1')
if (initialEscalationGameDeposit <= 0n) throw new Error('Protocol config initialEscalationGameDeposit must be greater than 0')
return {
forkBurnDivisor,
forkThresholdDivisor,
initialEscalationGameDeposit,
}
}

export function getProtocolConfig(overrides: ProtocolConfigInput = {}): ProtocolConfig {
return validateProtocolConfig({
...DEFAULT_PROTOCOL_CONFIG,
...getEnvironmentProtocolConfigOverrides(),
...getGlobalProtocolConfigOverrides(),
...overrides,
})
}
27 changes: 27 additions & 0 deletions solidity/contracts/SafeERC20Ops.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: Unlicense
pragma solidity 0.8.35;

import { IERC20 } from './IERC20.sol';

library SafeERC20Ops {
function safeApprove(IERC20 token, address spender, uint256 amount) internal {
_callOptionalReturn(token, abi.encodeCall(IERC20.approve, (spender, amount)));
}

function safeTransfer(IERC20 token, address receiver, uint256 amount) internal {
_callOptionalReturn(token, abi.encodeCall(IERC20.transfer, (receiver, amount)));
}

function safeTransferFrom(IERC20 token, address sender, address receiver, uint256 amount) internal {
_callOptionalReturn(token, abi.encodeCall(IERC20.transferFrom, (sender, receiver, amount)));
}

function _callOptionalReturn(IERC20 token, bytes memory callData) private {
require(address(token).code.length > 0, 'token missing code');
(bool success, bytes memory returnData) = address(token).call(callData);
require(success, 'token call failed');
if (returnData.length > 0) {
require(abi.decode(returnData, (bool)), 'token returned false');
}
}
}
23 changes: 15 additions & 8 deletions solidity/contracts/Zoltar.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
pragma solidity 0.8.35;

import './Constants.sol';
import './IERC20.sol';
import './ReputationToken.sol';
import './SafeERC20Ops.sol';
import './ZoltarQuestionData.sol';

uint256 constant FORK_THRESHOLD_DIVISOR = 20; // TODO, revisit, 5% of total supply atm
uint256 constant FORK_BURN_DIVISOR = 5; // TODO, revisit, 20% of fork threshold

contract Zoltar {
using SafeERC20Ops for IERC20;

struct Universe {
uint256 forkTime;
uint256 forkQuestionId;
Expand All @@ -32,10 +33,16 @@ contract Zoltar {
event UniverseForked(address forker, uint248 universeId, uint256 questionId);
event DeployChild(address deployer, uint248 universeId, uint256 outcomeIndex, uint248 childUniverseId, ReputationToken childReputationToken);

uint256 public immutable forkThresholdDivisor;
uint256 public immutable forkBurnDivisor;
ZoltarQuestionData public zoltarQuestionData;

constructor(ZoltarQuestionData _zoltarQuestionData) {
constructor(ZoltarQuestionData _zoltarQuestionData, uint256 _forkThresholdDivisor, uint256 _forkBurnDivisor) {
require(_forkThresholdDivisor > 1, 'fork threshold divisor');
require(_forkBurnDivisor > 1, 'fork burn divisor');
zoltarQuestionData = _zoltarQuestionData;
forkThresholdDivisor = _forkThresholdDivisor;
forkBurnDivisor = _forkBurnDivisor;
universes[0] = Universe(0, 0, 0, ReputationToken(Constants.GENESIS_REPUTATION_TOKEN), 0);
if (Constants.GENESIS_REPUTATION_TOKEN.code.length != 0) {
// The configured genesis token must expose `getTotalTheoreticalSupply()`.
Expand All @@ -56,7 +63,7 @@ contract Zoltar {
}

function getForkThreshold(uint248 universeId) public view returns (uint256) {
return getUniverseTheoreticalSupply(universeId) / FORK_THRESHOLD_DIVISOR;
return getUniverseTheoreticalSupply(universeId) / forkThresholdDivisor;
}

function getUniverseTheoreticalSupply(uint248 universeId) public view returns (uint256) {
Expand All @@ -79,17 +86,17 @@ contract Zoltar {
burnRep(universes[universeId].reputationToken, msg.sender, forkThreshold);
universeTheoreticalSupplies[universeId] -= forkThreshold;
childUniverseTheoreticalSupplySnapshots[universeId] = universeTheoreticalSupplies[universeId];
migrationRepBalances[msg.sender][universeId].migrationRepBalance = forkThreshold - forkThreshold / FORK_BURN_DIVISOR; // burn 20%
migrationRepBalances[msg.sender][universeId].migrationRepBalance = forkThreshold - forkThreshold / forkBurnDivisor;
emit UniverseForked(msg.sender, universeId, questionId);
}

function burnRep(ReputationToken reputationToken, address migrator, uint256 amount) private {
// Genesis is using REPv2 which we cannot actually burn
if (address(reputationToken) == Constants.GENESIS_REPUTATION_TOKEN) {
if (migrator == address(this)) {
reputationToken.transfer(Constants.BURN_ADDRESS, amount);
IERC20(address(reputationToken)).safeTransfer(Constants.BURN_ADDRESS, amount);
} else {
reputationToken.transferFrom(migrator, Constants.BURN_ADDRESS, amount);
IERC20(address(reputationToken)).safeTransferFrom(migrator, Constants.BURN_ADDRESS, amount);
}
} else {
ReputationToken(address(reputationToken)).burn(migrator, amount);
Expand Down
Loading
Loading