文件 1 的 5:Address.sol
pragma solidity ^0.8.0;
library Address {
function isContract(address account) internal view returns (bool) {
uint256 size;
assembly {
size := extcodesize(account)
}
return size > 0;
}
function sendValue(address payable recipient, uint256 amount) internal {
require(address(this).balance >= amount, "Address: insufficient balance");
(bool success, ) = recipient.call{value: amount}("");
require(success, "Address: unable to send value, recipient may have reverted");
}
function functionCall(address target, bytes memory data) internal returns (bytes memory) {
return functionCall(target, data, "Address: low-level call failed");
}
function functionCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0, errorMessage);
}
function functionCallWithValue(
address target,
bytes memory data,
uint256 value
) internal returns (bytes memory) {
return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
}
function functionCallWithValue(
address target,
bytes memory data,
uint256 value,
string memory errorMessage
) internal returns (bytes memory) {
require(address(this).balance >= value, "Address: insufficient balance for call");
require(isContract(target), "Address: call to non-contract");
(bool success, bytes memory returndata) = target.call{value: value}(data);
return verifyCallResult(success, returndata, errorMessage);
}
function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
return functionStaticCall(target, data, "Address: low-level static call failed");
}
function functionStaticCall(
address target,
bytes memory data,
string memory errorMessage
) internal view returns (bytes memory) {
require(isContract(target), "Address: static call to non-contract");
(bool success, bytes memory returndata) = target.staticcall(data);
return verifyCallResult(success, returndata, errorMessage);
}
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
return functionDelegateCall(target, data, "Address: low-level delegate call failed");
}
function functionDelegateCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
require(isContract(target), "Address: delegate call to non-contract");
(bool success, bytes memory returndata) = target.delegatecall(data);
return verifyCallResult(success, returndata, errorMessage);
}
function verifyCallResult(
bool success,
bytes memory returndata,
string memory errorMessage
) internal pure returns (bytes memory) {
if (success) {
return returndata;
} else {
if (returndata.length > 0) {
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert(errorMessage);
}
}
}
}
文件 2 的 5:IERC20.sol
pragma solidity ^0.8.0;
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(
address sender,
address recipient,
uint256 amount
) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
文件 3 的 5:OpolisPay.sol
pragma solidity 0.8.5;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
error NotPermitted();
error NotWhitelisted();
error InvalidPayroll();
error DuplicatePayroll();
error InvalidAmount();
error NotMember();
error InvalidStake();
error InvalidWithdraw();
error ZeroAddress();
error InvalidToken();
error ZeroTokens();
error AlreadyWhitelisted();
error DirectTransfer();
error LengthMismatch();
contract OpolisPay is ReentrancyGuard {
using SafeERC20 for IERC20;
address internal constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
address internal constant ZERO = address(0);
address[] public supportedTokens;
address private opolisAdmin;
address private opolisHelper;
address private ethLiquidation;
event SetupComplete(
address indexed admin,
address indexed helper,
address ethLiquidation,
address[] tokens,
address[] liqDestinations
);
event Staked(
address indexed staker, address indexed token, uint256 amount, uint256 indexed memberId, uint256 stakeNumber
);
event Paid(address indexed payor, address indexed token, uint256 indexed payrollId, uint256 amount);
event OpsPayrollWithdraw(address indexed token, uint256 indexed payrollId, uint256 amount);
event OpsStakeWithdraw(address indexed token, uint256 indexed stakeId, uint256 stakeNumber, uint256 amount);
event Sweep(address indexed token, uint256 amount);
event NewDestination(address indexed oldDestination, address indexed token, address indexed destination);
event NewAdmin(address indexed oldAdmin, address indexed opolisAdmin);
event NewHelper(address indexed oldHelper, address indexed newHelper);
event NewTokens(address[] newTokens, address[] newDestinations);
mapping(uint256 => uint256) private stakes;
mapping(uint256 => bool) private payrollIds;
mapping(uint256 => bool) public payrollWithdrawn;
mapping(uint256 => uint256) public stakeWithdrawn;
mapping(address => bool) public whitelisted;
mapping(address => address) public liqDestinations;
modifier onlyAdmin() {
if (msg.sender != opolisAdmin) revert NotPermitted();
_;
}
modifier onlyOpolis() {
if (!(msg.sender == opolisAdmin || msg.sender == opolisHelper)) {
revert NotPermitted();
}
_;
}
constructor(
address _opolisAdmin,
address _opolisHelper,
address _ethLiq,
address[] memory _tokenList,
address[] memory _destinationList
) {
if (_tokenList.length != _destinationList.length) revert LengthMismatch();
opolisAdmin = _opolisAdmin;
opolisHelper = _opolisHelper;
ethLiquidation = _ethLiq;
for (uint256 i = 0; i < _tokenList.length; i++) {
_addToken(_tokenList[i]);
_addDestination(_destinationList[i], _tokenList[i]);
}
emit SetupComplete(opolisAdmin, opolisHelper, _ethLiq, _tokenList, _destinationList);
}
function payPayroll(address token, uint256 amount, uint256 payrollId) external nonReentrant {
if (!whitelisted[token]) revert NotWhitelisted();
if (payrollId == 0) revert InvalidPayroll();
if (amount == 0) revert InvalidAmount();
if (payrollIds[payrollId]) revert DuplicatePayroll();
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
payrollIds[payrollId] = true;
emit Paid(msg.sender, token, payrollId, amount);
}
function memberStake(address token, uint256 amount, uint256 memberId) public payable nonReentrant {
if (!((whitelisted[token] && amount != 0) || (token == ETH && msg.value != 0))) {
revert InvalidStake();
}
if (memberId == 0) revert NotMember();
uint256 stakeCount = ++stakes[memberId];
if (msg.value > 0 && token == ETH) {
payable(ethLiquidation).transfer(msg.value);
emit Staked(msg.sender, ETH, msg.value, memberId, stakeCount);
} else {
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
emit Staked(msg.sender, token, amount, memberId, stakeCount);
}
}
function withdrawPayrolls(
uint256[] calldata _payrollIds,
address[] calldata _payrollTokens,
uint256[] calldata _payrollAmounts
) external onlyOpolis {
uint256[] memory withdrawAmounts = new uint256[](
supportedTokens.length
);
for (uint256 i = 0; i < _payrollIds.length; i++) {
uint256 id = _payrollIds[i];
if (!payrollIds[id]) revert InvalidPayroll();
address token = _payrollTokens[i];
uint256 amount = _payrollAmounts[i];
if (!payrollWithdrawn[id]) {
uint256 j;
for (; j < supportedTokens.length; j++) {
if (supportedTokens[j] == token) {
withdrawAmounts[j] += amount;
break;
}
}
if (j == supportedTokens.length) revert InvalidToken();
payrollWithdrawn[id] = true;
emit OpsPayrollWithdraw(token, id, amount);
}
}
for (uint256 i = 0; i < withdrawAmounts.length; i++) {
uint256 amount = withdrawAmounts[i];
if (amount > 0) {
_withdraw(supportedTokens[i], amount);
}
}
}
function withdrawStakes(
uint256[] calldata _stakeIds,
uint256[] calldata _stakeNum,
address[] calldata _stakeTokens,
uint256[] calldata _stakeAmounts
) external onlyOpolis {
uint256[] memory withdrawAmounts = new uint256[](
supportedTokens.length
);
if (_stakeIds.length != _stakeNum.length) revert InvalidWithdraw();
for (uint256 i = 0; i < _stakeIds.length; i++) {
uint256 id = _stakeIds[i];
address token = _stakeTokens[i];
uint256 amount = _stakeAmounts[i];
uint256 num = _stakeNum[i];
if (stakeWithdrawn[id] < num) {
uint256 j;
for (; j < supportedTokens.length; j++) {
if (supportedTokens[j] == token) {
withdrawAmounts[j] += amount;
break;
}
}
if (j == supportedTokens.length) revert InvalidToken();
stakeWithdrawn[id] = num;
emit OpsStakeWithdraw(token, id, num, amount);
}
}
for (uint256 i = 0; i < withdrawAmounts.length; i++) {
uint256 amount = withdrawAmounts[i];
if (amount > 0) {
_withdraw(supportedTokens[i], amount);
}
}
}
function clearBalance() public onlyAdmin {
for (uint256 i = 0; i < supportedTokens.length; i++) {
address token = supportedTokens[i];
uint256 balance = IERC20(token).balanceOf(address(this));
if (balance > 0) {
_withdraw(token, balance);
}
emit Sweep(token, balance);
}
}
receive() external payable {
revert DirectTransfer();
}
function updateDestination(address token, address newDestination) external onlyAdmin {
if (newDestination == ZERO) revert ZeroAddress();
address oldDestination = liqDestinations[token];
liqDestinations[token] = newDestination;
emit NewDestination(oldDestination, token, newDestination);
}
function updateAdmin(address newAdmin) external onlyAdmin returns (address) {
if (newAdmin == ZERO) revert ZeroAddress();
emit NewAdmin(opolisAdmin, newAdmin);
opolisAdmin = newAdmin;
return opolisAdmin;
}
function updateHelper(address newHelper) external onlyAdmin returns (address) {
if (newHelper == ZERO) revert ZeroAddress();
emit NewHelper(opolisHelper, newHelper);
opolisHelper = newHelper;
return opolisHelper;
}
function addTokens(address[] memory newTokens, address[] memory newDestinations) external onlyAdmin {
if (newTokens.length == 0) revert ZeroTokens();
if (newTokens.length != newDestinations.length) revert LengthMismatch();
for (uint256 i = 0; i < newTokens.length; i++) {
_addToken(newTokens[i]);
_addDestination(newDestinations[i], newTokens[i]);
}
emit NewTokens(newTokens, newDestinations);
}
function _addToken(address token) internal {
if (whitelisted[token]) revert AlreadyWhitelisted();
if (token == ZERO) revert ZeroAddress();
supportedTokens.push(token);
whitelisted[token] = true;
}
function _addDestination(address destination, address token) internal {
if (destination == ZERO) revert ZeroAddress();
liqDestinations[token] = destination;
}
function _withdraw(address token, uint256 amount) internal {
address dest = liqDestinations[token];
IERC20(token).safeTransfer(dest, amount);
}
}
文件 4 的 5:ReentrancyGuard.sol
pragma solidity ^0.8.0;
abstract contract ReentrancyGuard {
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
constructor() {
_status = _NOT_ENTERED;
}
modifier nonReentrant() {
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
_status = _ENTERED;
_;
_status = _NOT_ENTERED;
}
}
文件 5 的 5:SafeERC20.sol
pragma solidity ^0.8.0;
import "../IERC20.sol";
import "../../../utils/Address.sol";
library SafeERC20 {
using Address for address;
function safeTransfer(
IERC20 token,
address to,
uint256 value
) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
}
function safeTransferFrom(
IERC20 token,
address from,
address to,
uint256 value
) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
}
function safeApprove(
IERC20 token,
address spender,
uint256 value
) internal {
require(
(value == 0) || (token.allowance(address(this), spender) == 0),
"SafeERC20: approve from non-zero to non-zero allowance"
);
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
}
function safeIncreaseAllowance(
IERC20 token,
address spender,
uint256 value
) internal {
uint256 newAllowance = token.allowance(address(this), spender) + value;
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
}
function safeDecreaseAllowance(
IERC20 token,
address spender,
uint256 value
) internal {
unchecked {
uint256 oldAllowance = token.allowance(address(this), spender);
require(oldAllowance >= value, "SafeERC20: decreased allowance below zero");
uint256 newAllowance = oldAllowance - value;
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
}
}
function _callOptionalReturn(IERC20 token, bytes memory data) private {
bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
if (returndata.length > 0) {
require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
}
}
}
{
"compilationTarget": {
"contracts/OpolisPay.sol": "OpolisPay"
},
"evmVersion": "berlin",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": []
}
[{"inputs":[{"internalType":"address","name":"_opolisAdmin","type":"address"},{"internalType":"address","name":"_opolisHelper","type":"address"},{"internalType":"address","name":"_ethLiq","type":"address"},{"internalType":"address[]","name":"_tokenList","type":"address[]"},{"internalType":"address[]","name":"_destinationList","type":"address[]"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AlreadyWhitelisted","type":"error"},{"inputs":[],"name":"DirectTransfer","type":"error"},{"inputs":[],"name":"DuplicatePayroll","type":"error"},{"inputs":[],"name":"InvalidAmount","type":"error"},{"inputs":[],"name":"InvalidPayroll","type":"error"},{"inputs":[],"name":"InvalidStake","type":"error"},{"inputs":[],"name":"InvalidToken","type":"error"},{"inputs":[],"name":"InvalidWithdraw","type":"error"},{"inputs":[],"name":"LengthMismatch","type":"error"},{"inputs":[],"name":"NotMember","type":"error"},{"inputs":[],"name":"NotPermitted","type":"error"},{"inputs":[],"name":"NotWhitelisted","type":"error"},{"inputs":[],"name":"ZeroAddress","type":"error"},{"inputs":[],"name":"ZeroTokens","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"oldAdmin","type":"address"},{"indexed":true,"internalType":"address","name":"opolisAdmin","type":"address"}],"name":"NewAdmin","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"oldDestination","type":"address"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":true,"internalType":"address","name":"destination","type":"address"}],"name":"NewDestination","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"oldHelper","type":"address"},{"indexed":true,"internalType":"address","name":"newHelper","type":"address"}],"name":"NewHelper","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address[]","name":"newTokens","type":"address[]"},{"indexed":false,"internalType":"address[]","name":"newDestinations","type":"address[]"}],"name":"NewTokens","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":true,"internalType":"uint256","name":"payrollId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"OpsPayrollWithdraw","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":true,"internalType":"uint256","name":"stakeId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"stakeNumber","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"OpsStakeWithdraw","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"payor","type":"address"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":true,"internalType":"uint256","name":"payrollId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Paid","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"admin","type":"address"},{"indexed":true,"internalType":"address","name":"helper","type":"address"},{"indexed":false,"internalType":"address","name":"ethLiquidation","type":"address"},{"indexed":false,"internalType":"address[]","name":"tokens","type":"address[]"},{"indexed":false,"internalType":"address[]","name":"liqDestinations","type":"address[]"}],"name":"SetupComplete","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"staker","type":"address"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"memberId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"stakeNumber","type":"uint256"}],"name":"Staked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Sweep","type":"event"},{"inputs":[{"internalType":"address[]","name":"newTokens","type":"address[]"},{"internalType":"address[]","name":"newDestinations","type":"address[]"}],"name":"addTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"clearBalance","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"liqDestinations","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"memberId","type":"uint256"}],"name":"memberStake","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"payrollId","type":"uint256"}],"name":"payPayroll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"payrollWithdrawn","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"stakeWithdrawn","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"supportedTokens","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newAdmin","type":"address"}],"name":"updateAdmin","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"address","name":"newDestination","type":"address"}],"name":"updateDestination","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newHelper","type":"address"}],"name":"updateHelper","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"whitelisted","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_payrollIds","type":"uint256[]"},{"internalType":"address[]","name":"_payrollTokens","type":"address[]"},{"internalType":"uint256[]","name":"_payrollAmounts","type":"uint256[]"}],"name":"withdrawPayrolls","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_stakeIds","type":"uint256[]"},{"internalType":"uint256[]","name":"_stakeNum","type":"uint256[]"},{"internalType":"address[]","name":"_stakeTokens","type":"address[]"},{"internalType":"uint256[]","name":"_stakeAmounts","type":"uint256[]"}],"name":"withdrawStakes","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]