Description:
ERC20 token contract with Factory capabilities. Standard implementation for fungible tokens on Ethereum.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"npm/@openzeppelin/contracts@5.4.0/token/ERC20/IERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/IERC20.sol)
pragma solidity >=0.4.16;
/**
* @dev Interface of the ERC-20 standard as defined in the ERC.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the value of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the value of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 value) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the
* allowance mechanism. `value` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
"
},
"project/contracts/KnineRecoveryBountyDecayAcceptMultiFunder.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/**
* @title KnineRecoveryBountyDecayAcceptMultiFunder
* @author Shima @ K9 Finance DAO
* @notice Exploiter can `accept()` to freeze decay. `recoverKnine()` pays based on freeze (or now)
* and returns KNINE to the Shibarium Bridge. Remaining ETH is refunded pro‑rata to funders
* via a snapshot + batched distribution with pull fallback.
*
* @dev Multiple funders can send ETH to the contract after deployment to increase the bounty pool.
* TermsURI: ipfs://bafkreifp7aycps7lozpln4y2mcpyksei6lgiu3ylosr7vuznjcajqhjwzu
*/
contract KnineRecoveryBountyDecayAcceptMultiFunder {
// ===== Constants =====
string public constant IPFS_TERMS_URI = "ipfs://bafkreifp7aycps7lozpln4y2mcpyksei6lgiu3ylosr7vuznjcajqhjwzu";
IERC20 public constant KNINE =
IERC20(0x91fbB2503AC69702061f1AC6885759Fc853e6EaE);
address public constant EXPLOITER =
0x999E025a2a0558c07DBf7F021b2C9852B367e80A;
/// @notice Shibarium Bridge (ERC20PredicateProxy)
/// Location where KNINE will be returned to when calling `recoverKnine()`
/// See: https://etherscan.io/address/0x6aca26bfce7675ff71c734bf26c8c0ac4039a4fa#code
address public constant SHIBARIUM_BRIDGE =
0x6Aca26bFCE7675FF71C734BF26C8c0aC4039A4Fa;
/// @notice 248.9894 Billion KNINE (with 18 decimals)
uint256 public constant AMOUNT = 248989400000000000000000000000;
/**
* @notice Minimum funding amount to be considered a funder.
* @dev Any ETH sent below this amount will be rejected.
* Prevents dust funders that would complicate proportional refunds.
*/
uint256 public constant MIN_FUNDING = 0.01 ether;
// ===== Timeline =====
/// @notice Bounty claim start timestamp
/// @dev starts immediately on contract deployment
uint256 public immutable START;
/// @notice (optional) initial claim window (in seconds), before reward decay starts, where exploiter can claim 100% of the bounty
uint256 public immutable INITIAL;
/// @notice Time window for bounty claim (in seconds) during which available claim decreases linearly
uint256 public immutable DECAY;
/// @notice Keccak256 of human‑readable terms (e.g., IPFS text) for safe‑harbor / scope.
/// @dev 0xdc41ed1a9106d5b1a5325e996240b1d76ee437ead8b8471e627f9b53ad2d3d1f
bytes32 public immutable TERMS_HASH;
// ===== Acceptance / finalization =====
/// @notice @notice Timestamp (if any) at which the exploiter froze the decay; 0 if not accepted.
uint256 public acceptedAt;
/// @notice True once KNINE is successfully recovered and bounty paid.
bool public finalized;
/* ====== Multi‑Funder Accounting ====== */
mapping(address => uint256) public fundedAmounts;
address[] public funders;
uint256 public totalFunded;
// ===== Refund state =====
bool public refundsEnabled;
uint256 public refundSnapshot; // total ETH to refund, frozen when `_enableRefunds` is called
/// @dev Cursor for batched refund processing, in case not all funders can be processed in one tx (due to gas limits)
uint256 public refundCursor; // next index in funders[] to process
mapping(address => uint256) public refunded; // total credited to funder (target they've reached)
mapping(address => uint256) public owed; // push failures accumulate here
// ===== Minimal reentrancy guard =====
uint256 private _unlocked = 1;
modifier nonReentrant() {
require(_unlocked == 1, "REENTRANCY");
_unlocked = 0;
_;
_unlocked = 1;
}
// ====== Events =======
event BountyFunded(address indexed funder, uint256 amount);
event Accepted(uint256 at, bytes32 termsHash);
event DealFinalized(
address indexed exploiter,
uint256 paidEth,
bytes32 termsHash
);
event RefundsEnabled(uint256 snapshotAmount);
event Refunded(address indexed to, uint256 amount);
event RefundCreditRecorded(address indexed to, uint256 amount);
/**
*
* @param initialPeriod seconds to set initial (100% bounty claim) window.
* @param decayPeriod seconds to set for the decay claim windown
* @param termsHash keccak256 of public terms text hosted in IPFS
*/
constructor(uint256 initialPeriod, uint256 decayPeriod, bytes32 termsHash) {
require(decayPeriod > 0, "BAD_DECAY");
START = block.timestamp;
INITIAL = initialPeriod;
DECAY = decayPeriod;
TERMS_HASH = termsHash;
}
/**
* @notice Exploiter calls to accept bounty and freeze the decay at the current time by showing readiness.
* Once accepted, as long as exploiter does not remove allowance, bounty cannot be revoked or reneged by K9 Finance DAO
* @dev Requires (1) not finalized; (2) called by exploiter; (3) allowance >= AMOUNT; (4) not already accepted
*/
function accept() external {
require(block.timestamp < START + INITIAL + DECAY, "TOO_LATE");
require(!finalized, "FINALIZED");
require(msg.sender == EXPLOITER, "ONLY_EXPLOITER");
require(
KNINE.allowance(EXPLOITER, address(this)) >= AMOUNT,
"ALLOWANCE"
);
require(acceptedAt == 0, "ACK");
acceptedAt = block.timestamp;
emit Accepted(acceptedAt, TERMS_HASH);
}
/// @dev Returns the ETH payout if executed at timestamp `ts`.
/// @return payoutAmount amount of ETH (in wei) to pay out
function _payoutAt(
uint256 ts
) internal view returns (uint256 payoutAmount) {
payoutAmount = address(this).balance;
uint256 t = (ts > START) ? (ts - START) : 0;
if (t <= INITIAL) return payoutAmount;
if (t >= INITIAL + DECAY) return 0;
return (payoutAmount * (INITIAL + DECAY - t)) / DECAY;
}
/**
* @notice Pulls KNINE into `SHIBARIUM_BRIDGE` and pays the ETH bounty to exploiter.
* @dev Uses `acceptedAt` if present (exploiter called accept), else `block.timestamp`.
* Sets `finalized` BEFORE sending ETH to prevent re‑acceptance via callback.
*/
function recoverKnine() external {
require(!finalized, "FINALIZED");
uint256 ref = (acceptedAt > 0) ? acceptedAt : block.timestamp;
uint256 pay = _payoutAt(ref);
require(pay > 0, "EXPIRED");
uint balStart = KNINE.balanceOf(SHIBARIUM_BRIDGE);
require(
KNINE.transferFrom(EXPLOITER, SHIBARIUM_BRIDGE, AMOUNT),
"TRANSFER_FAIL"
);
if (KNINE.balanceOf(SHIBARIUM_BRIDGE) < balStart + AMOUNT) {
revert("wtf"); // super duper check that we got the KNINE back
}
// Prevent any re‑acceptance during ETH send.
// will fail if exploiter tries reentrancy (using 7702 magic, EOA to contract shinanigans)
finalized = true;
(bool ok, ) = payable(EXPLOITER).call{value: pay}("");
require(ok, "ETH_PAY_FAIL");
emit DealFinalized(EXPLOITER, pay, TERMS_HASH);
_enableRefunds(); // enable refunds must happen after finalization
}
/* ======= Multi‑Funder ETH Handling ======= */
/// @notice Allow funding contract with ETH bounty after creation,
/// tracking amounts contributed by different addresses.
/// @dev only accept funding above `MIN_FUNDING` to avoid dust funders.
/// rejects any further funding if finalized, refunds started, or time expired.
receive() external payable {
require(!finalized, "FINALIZED");
require(!refundsEnabled, "REFUNDS_STARTED");
require(timeRemaining() > 0, "FUNDING_CLOSED");
require(msg.value >= MIN_FUNDING, "MIN_FUNDING");
if (fundedAmounts[msg.sender] == 0) {
funders.push(msg.sender);
}
fundedAmounts[msg.sender] += msg.value;
totalFunded += msg.value;
emit BountyFunded(msg.sender, msg.value);
}
// ===== Refund Logic =====
function _enableRefunds() internal {
if (refundsEnabled) return; // already enabled, no-op
require(_canBeginRefunds(), "LOCKED_OR_EARLY");
refundsEnabled = true; // allow refunds & block receive from accepting any more ETH
refundSnapshot = address(this).balance; // record snapshot of contract balance to split for pro-rata refunds
emit RefundsEnabled(refundSnapshot);
}
function _canBeginRefunds() internal view returns (bool) {
if (finalized) return true; // after exploiter is paid
if (block.timestamp < START + INITIAL + DECAY) return false;
// Prevent reneging while a valid acceptance is still in force
if (acceptedAt > 0) {
if (
KNINE.allowance(EXPLOITER, address(this)) >= AMOUNT &&
KNINE.balanceOf(EXPLOITER) >= AMOUNT
) {
return false;
}
}
return true;
}
function refundAllEth() external nonReentrant {
_refundBatch(funders.length);
}
/// @notice Processes up to `batchSize` funders and returns their pro‑rata share of `refundSnapshot`.
/// Best‑effort push; failures are credited to `owed` for pull‑based claiming.
/// @param batchSize maximum number of funders to process in this call
function refundBatch(uint256 batchSize) external nonReentrant {
_refundBatch(batchSize);
}
function _refundBatch(uint256 batchSize) internal {
require(batchSize > 0, "BAD_BATCH_SIZE");
if (!refundsEnabled) {
_enableRefunds();
}
uint256 n = funders.length;
if (n == 0) {
revert("NO_FUNDERS");
}
// we use refundCursor to track progress working through all the funders
// that need refunding, incase we cannot do them all in a single batch due
// to gas limits.
// We will process funders[refundCursor..refundCursor+batchSize]
uint256 i = refundCursor;
uint256 end = i + batchSize;
if (end > n) end = n;
uint256 totalFundedLocal = totalFunded; // gas savings instead of SLOAD in loop
uint256 refundSnapshotLocal = refundSnapshot;
for (; i < end; i++) {
address a = funders[i];
uint256 target = (fundedAmounts[a] * refundSnapshotLocal) / totalFundedLocal; // full allocation
uint256 already = refunded[a];
if (target <= already) continue;
uint256 due = target - already;
// effects first
refunded[a] = target;
(bool ok, ) = payable(a).call{value: due}("");
if (!ok) {
owed[a] += due;
emit RefundCreditRecorded(a, due);
} else {
emit Refunded(a, due);
}
}
refundCursor = i;
// any rounding dust remains in the contract permanently... but whatever
}
/**
* @notice Pull any unpaid refund (from a failed push or after batching).
*/
function claimRefund() external nonReentrant {
require(refundsEnabled, "REFUNDS_NOT_ENABLED");
if (totalFunded == 0) revert("NO_FUNDERS");
uint256 target = (fundedAmounts[msg.sender] * refundSnapshot) /
totalFunded;
uint256 already = refunded[msg.sender];
uint256 pushDue = 0;
if (target > already) {
pushDue = target - already;
refunded[msg.sender] = target; // effects first
}
uint256 extra = owed[msg.sender];
if (extra > 0) {
owed[msg.sender] = 0; // effects first
}
uint256 amount = pushDue + extra;
require(amount > 0, "NOTHING_DUE");
(bool ok, ) = payable(msg.sender).call{value: amount}("");
require(ok, "CLAIM_FAIL");
emit Refunded(msg.sender, amount);
}
/* ======= View Helpers ======= */
/// @notice Check how much refund is currently due to `who`
function refundOwed(address who) external view returns (uint256) {
if (!refundsEnabled || totalFunded == 0) return 0;
uint256 target = (fundedAmounts[who] * refundSnapshot) / totalFunded;
uint256 already = refunded[who];
return (target > already ? target - already : 0) + owed[who];
}
/// @notice Convenience function to check time remaining for bounty claim
function timeRemaining() public view returns (uint256) {
if (block.timestamp >= START + INITIAL + DECAY) {
return 0;
} else {
return (START + INITIAL + DECAY) - block.timestamp;
}
}
/// @notice Convenience function to check current payout amount
function currentPayout() external view returns (uint256) {
uint256 ref = (acceptedAt > 0) ? acceptedAt : block.timestamp;
return _payoutAt(ref);
}
}
"
}
},
"settings": {
"viaIR": true,
"optimizer": {
"runs": 200,
"enabled": true
},
"evmVersion": "cancun",
"remappings": [
"project/:@openzeppelin/contracts/=npm/@openzeppelin/contracts@5.4.0/"
],
"outputSelection": {
"*": {
"*": [
"evm.bytecode",
"evm.deployedBytecode",
"devdoc",
"userdoc",
"metadata",
"abi"
]
}
}
}
}}
Submitted on: 2025-10-29 20:01:18
Comments
Log in to comment.
No comments yet.