268 lines
8.4 KiB
Solidity
268 lines
8.4 KiB
Solidity
// SPDX-License-Identifier: AGPL-3.0-only
|
|
pragma solidity ^0.8.24;
|
|
|
|
interface IERC20 {
|
|
function transferFrom(address from, address to, uint256 value) external returns (bool);
|
|
function transfer(address to, uint256 value) external returns (bool);
|
|
}
|
|
|
|
library Address {
|
|
function functionCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) {
|
|
(bool success, bytes memory returndata) = target.call(data);
|
|
require(success, errorMessage);
|
|
return returndata;
|
|
}
|
|
}
|
|
|
|
library SafeERC20 {
|
|
using Address for address;
|
|
|
|
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
|
|
bytes memory returndata = address(token).functionCall(
|
|
abi.encodeWithSelector(token.transferFrom.selector, from, to, value),
|
|
"SAFE_TRANSFER_FROM_FAILED"
|
|
);
|
|
if (returndata.length > 0) {
|
|
require(abi.decode(returndata, (bool)), "SAFE_TRANSFER_FROM_FALSE");
|
|
}
|
|
}
|
|
|
|
function safeTransfer(IERC20 token, address to, uint256 value) internal {
|
|
bytes memory returndata = address(token).functionCall(
|
|
abi.encodeWithSelector(token.transfer.selector, to, value),
|
|
"SAFE_TRANSFER_FAILED"
|
|
);
|
|
if (returndata.length > 0) {
|
|
require(abi.decode(returndata, (bool)), "SAFE_TRANSFER_FALSE");
|
|
}
|
|
}
|
|
}
|
|
|
|
abstract contract Ownable {
|
|
address public owner;
|
|
|
|
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
|
|
|
|
modifier onlyOwner() {
|
|
require(msg.sender == owner, "ONLY_OWNER");
|
|
_;
|
|
}
|
|
|
|
constructor(address initialOwner) {
|
|
require(initialOwner != address(0), "ZERO_OWNER");
|
|
owner = initialOwner;
|
|
emit OwnershipTransferred(address(0), initialOwner);
|
|
}
|
|
|
|
function transferOwnership(address newOwner) external onlyOwner {
|
|
require(newOwner != address(0), "ZERO_OWNER");
|
|
emit OwnershipTransferred(owner, newOwner);
|
|
owner = newOwner;
|
|
}
|
|
}
|
|
|
|
abstract contract Pausable {
|
|
bool public paused;
|
|
|
|
event Paused(address indexed account);
|
|
event Unpaused(address indexed account);
|
|
|
|
modifier whenNotPaused() {
|
|
require(!paused, "PAUSED");
|
|
_;
|
|
}
|
|
|
|
function _pause() internal {
|
|
require(!paused, "PAUSED");
|
|
paused = true;
|
|
emit Paused(msg.sender);
|
|
}
|
|
|
|
function _unpause() internal {
|
|
require(paused, "NOT_PAUSED");
|
|
paused = false;
|
|
emit Unpaused(msg.sender);
|
|
}
|
|
}
|
|
|
|
abstract contract ReentrancyGuard {
|
|
uint256 private _status = 1;
|
|
|
|
modifier nonReentrant() {
|
|
require(_status == 1, "REENTRANT");
|
|
_status = 2;
|
|
_;
|
|
_status = 1;
|
|
}
|
|
}
|
|
|
|
contract PolyWeatherCheckoutV2 is Ownable, Pausable, ReentrancyGuard {
|
|
using SafeERC20 for IERC20;
|
|
|
|
struct PlanConfig {
|
|
uint256 amount;
|
|
bool active;
|
|
}
|
|
|
|
bytes32 public constant AUTHORIZED_PAYMENT_TYPEHASH =
|
|
keccak256(
|
|
"AuthorizedPayment(bytes32 orderId,address payer,uint256 planId,address token,uint256 amount,uint256 nonce,uint256 deadline)"
|
|
);
|
|
|
|
bytes32 public immutable DOMAIN_SEPARATOR;
|
|
|
|
address public treasury;
|
|
address public signer;
|
|
mapping(address => bool) public allowedToken;
|
|
mapping(bytes32 => bool) public paidOrder;
|
|
mapping(uint256 => mapping(address => PlanConfig)) public planConfig;
|
|
mapping(address => uint256) public payerNonce;
|
|
|
|
event OrderPaid(
|
|
bytes32 indexed orderId,
|
|
address indexed payer,
|
|
uint256 indexed planId,
|
|
address token,
|
|
uint256 amount
|
|
);
|
|
event TreasuryUpdated(address indexed treasury);
|
|
event SignerUpdated(address indexed signer);
|
|
event TokenAllowedUpdated(address indexed token, bool allowed);
|
|
event PlanConfigured(uint256 indexed planId, address indexed token, uint256 amount, bool active);
|
|
|
|
constructor(address initialOwner, address initialTreasury, address initialSigner)
|
|
Ownable(initialOwner)
|
|
{
|
|
require(initialTreasury != address(0), "ZERO_TREASURY");
|
|
treasury = initialTreasury;
|
|
signer = initialSigner;
|
|
|
|
uint256 chainId;
|
|
assembly {
|
|
chainId := chainid()
|
|
}
|
|
DOMAIN_SEPARATOR = keccak256(
|
|
abi.encode(
|
|
keccak256(
|
|
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
|
|
),
|
|
keccak256(bytes("PolyWeatherCheckoutV2")),
|
|
keccak256(bytes("1")),
|
|
chainId,
|
|
address(this)
|
|
)
|
|
);
|
|
}
|
|
|
|
function setTreasury(address newTreasury) external onlyOwner {
|
|
require(newTreasury != address(0), "ZERO_ADDR");
|
|
treasury = newTreasury;
|
|
emit TreasuryUpdated(newTreasury);
|
|
}
|
|
|
|
function setSigner(address newSigner) external onlyOwner {
|
|
signer = newSigner;
|
|
emit SignerUpdated(newSigner);
|
|
}
|
|
|
|
function setTokenAllowed(address token, bool allowed) external onlyOwner {
|
|
require(token != address(0), "ZERO_ADDR");
|
|
allowedToken[token] = allowed;
|
|
emit TokenAllowedUpdated(token, allowed);
|
|
}
|
|
|
|
function setPlan(uint256 planId, address token, uint256 amount, bool active) external onlyOwner {
|
|
require(planId > 0, "PLAN_ZERO");
|
|
require(token != address(0), "ZERO_ADDR");
|
|
require(amount > 0 || !active, "AMOUNT_ZERO");
|
|
planConfig[planId][token] = PlanConfig({amount: amount, active: active});
|
|
emit PlanConfigured(planId, token, amount, active);
|
|
}
|
|
|
|
function pause() external onlyOwner {
|
|
_pause();
|
|
}
|
|
|
|
function unpause() external onlyOwner {
|
|
_unpause();
|
|
}
|
|
|
|
function payPlan(bytes32 orderId, uint256 planId, address token)
|
|
external
|
|
whenNotPaused
|
|
nonReentrant
|
|
{
|
|
require(allowedToken[token], "TOKEN_NOT_ALLOWED");
|
|
PlanConfig memory config = planConfig[planId][token];
|
|
require(config.active, "PLAN_NOT_ACTIVE");
|
|
require(config.amount > 0, "PLAN_AMOUNT_ZERO");
|
|
_collect(orderId, msg.sender, planId, token, config.amount);
|
|
}
|
|
|
|
function payAuthorized(
|
|
bytes32 orderId,
|
|
uint256 planId,
|
|
address token,
|
|
uint256 amount,
|
|
uint256 deadline,
|
|
bytes calldata signature
|
|
) external whenNotPaused nonReentrant {
|
|
require(allowedToken[token], "TOKEN_NOT_ALLOWED");
|
|
require(amount > 0, "AMOUNT_ZERO");
|
|
require(deadline >= block.timestamp, "AUTH_EXPIRED");
|
|
require(signer != address(0), "SIGNER_NOT_SET");
|
|
|
|
uint256 nonce = payerNonce[msg.sender];
|
|
bytes32 structHash = keccak256(
|
|
abi.encode(
|
|
AUTHORIZED_PAYMENT_TYPEHASH,
|
|
orderId,
|
|
msg.sender,
|
|
planId,
|
|
token,
|
|
amount,
|
|
nonce,
|
|
deadline
|
|
)
|
|
);
|
|
bytes32 digest = keccak256(
|
|
abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash)
|
|
);
|
|
require(_recover(digest, signature) == signer, "BAD_SIGNATURE");
|
|
payerNonce[msg.sender] = nonce + 1;
|
|
|
|
_collect(orderId, msg.sender, planId, token, amount);
|
|
}
|
|
|
|
function rescueToken(address token, address to, uint256 amount) external onlyOwner nonReentrant {
|
|
require(token != address(0) && to != address(0), "ZERO_ADDR");
|
|
IERC20(token).safeTransfer(to, amount);
|
|
}
|
|
|
|
function _collect(bytes32 orderId, address payer, uint256 planId, address token, uint256 amount) internal {
|
|
require(!paidOrder[orderId], "ORDER_PAID");
|
|
paidOrder[orderId] = true;
|
|
IERC20(token).safeTransferFrom(payer, treasury, amount);
|
|
emit OrderPaid(orderId, payer, planId, token, amount);
|
|
}
|
|
|
|
function _recover(bytes32 digest, bytes calldata signature) internal pure returns (address) {
|
|
require(signature.length == 65, "BAD_SIG_LEN");
|
|
bytes32 r;
|
|
bytes32 s;
|
|
uint8 v;
|
|
assembly {
|
|
r := calldataload(signature.offset)
|
|
s := calldataload(add(signature.offset, 32))
|
|
v := byte(0, calldataload(add(signature.offset, 64)))
|
|
}
|
|
if (v < 27) {
|
|
v += 27;
|
|
}
|
|
require(v == 27 || v == 28, "BAD_SIG_V");
|
|
address recovered = ecrecover(digest, v, r, s);
|
|
require(recovered != address(0), "BAD_SIG");
|
|
return recovered;
|
|
}
|
|
}
|