mirror of
https://github.com/luxfi/uma.git
synced 2026-10-30 15:14:44 +00:00
267 lines
12 KiB
Solidity
267 lines
12 KiB
Solidity
// SPDX-License-Identifier: AGPL-3.0-only
|
|
pragma solidity 0.8.16;
|
|
|
|
import "./GovernorV2.sol";
|
|
import "../../common/implementation/Lockable.sol";
|
|
import "../../common/implementation/MultiCaller.sol";
|
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
|
|
/**
|
|
* @title Emergency Proposer contract
|
|
* @dev This is a contract that allows anyone to construct an emergency recovery transaction to bypass the
|
|
* standard voting process by submitting a very large bond, which is considered a quorum in this case. This bond is
|
|
* expected to be about as large as the GAT in the VotingV2 contract. If a proposal is considered invalid, UMA token
|
|
* holders can vote to slash and remove this proposal through the standard governance flow. If valid, a proposal must
|
|
* wait minimumWaitTime before it can be executed and it can only be executed by a privileged account, executor. This
|
|
* includes three tiers of protection to ensure that abuse is extremely risky both from creating market volatility in
|
|
* the underlying token and the threat of the locked tokens being slashed.
|
|
*/
|
|
contract EmergencyProposer is Ownable, Lockable, MultiCaller {
|
|
using SafeERC20 for IERC20;
|
|
|
|
/****************************************
|
|
* EMERGENCY PROPOSAL STATE *
|
|
****************************************/
|
|
|
|
// Identifies a unique emergency proposal.
|
|
struct EmergencyProposal {
|
|
address sender; // Sender of the proposal. Address that receives the bond refund in the case of execution.
|
|
uint64 expiryTime; // Time at which the proposal expires and can be executed.
|
|
uint256 lockedTokens; // Tokens locked for the proposal. Tokens are returned when proposal is executed.
|
|
GovernorV2.Transaction[] transactions; // Array of transactions to be executed in the emergency action.
|
|
}
|
|
|
|
// Array of all proposed emergency proposals.
|
|
EmergencyProposal[] public emergencyProposals;
|
|
|
|
// UMA Governor, used to execute transactions. The Governor is the owner of all other UMA ecosystem contracts.
|
|
GovernorV2 public immutable governor;
|
|
|
|
// Voting token, used to bond proposes.
|
|
IERC20 public immutable token;
|
|
|
|
// The number of tokens needed to propose an emergency action.
|
|
uint256 public quorum;
|
|
|
|
// The minimum time that must elapse between from when a proposal is created to when it can be executed.
|
|
uint64 public minimumWaitTime;
|
|
|
|
// The only address that can execute an emergency proposal. Will be set to a multisig. Acts to guardrail the
|
|
// emergency recovery mechanism and ensure that only valid proposals can be executed. Note that while this address
|
|
// is somewhat privileged, it can't unilaterally push through proposals as a proposal must pass the minimum wait
|
|
// time without the DVM voters voting to slash the proposal.
|
|
address public executor;
|
|
|
|
/****************************************
|
|
* EVENTS *
|
|
****************************************/
|
|
|
|
event QuorumSet(uint256 quorum);
|
|
event ExecutorSet(address executor);
|
|
event MinimumWaitTimeSet(uint256 minimumWaitTime);
|
|
event EmergencyTransactionsProposed(
|
|
uint256 indexed id,
|
|
address indexed sender,
|
|
address indexed caller,
|
|
uint64 expiryTime,
|
|
uint256 lockedTokens,
|
|
GovernorV2.Transaction[] transactions
|
|
);
|
|
event EmergencyProposalRemoved(
|
|
uint256 indexed id,
|
|
address indexed sender,
|
|
address indexed caller,
|
|
uint64 expiryTime,
|
|
uint256 lockedTokens,
|
|
GovernorV2.Transaction[] transactions
|
|
);
|
|
event EmergencyProposalSlashed(
|
|
uint256 indexed id,
|
|
address indexed sender,
|
|
address indexed caller,
|
|
uint64 expiryTime,
|
|
uint256 lockedTokens,
|
|
GovernorV2.Transaction[] transactions
|
|
);
|
|
event EmergencyProposalExecuted(
|
|
uint256 indexed id,
|
|
address indexed sender,
|
|
address indexed caller,
|
|
uint64 expiryTime,
|
|
uint256 lockedTokens,
|
|
GovernorV2.Transaction[] transactions
|
|
);
|
|
|
|
/**
|
|
* @notice Construct the EmergencyProposer contract.
|
|
* @param _token the ERC20 token that the quorum is in.
|
|
* @param _quorum the tokens needed to propose an emergency action.
|
|
* @param _governor the governor contract that this contract makes proposals to.
|
|
* @param _executor the address that can execute an emergency proposal.
|
|
* @param _minimumWaitTime the minimum time that must elapse between from when a proposal is created to when it can
|
|
* be executed.
|
|
*/
|
|
constructor(
|
|
IERC20 _token,
|
|
uint256 _quorum,
|
|
GovernorV2 _governor,
|
|
address _executor,
|
|
uint64 _minimumWaitTime
|
|
) {
|
|
token = _token;
|
|
governor = _governor;
|
|
setExecutor(_executor);
|
|
setQuorum(_quorum);
|
|
|
|
setMinimumWaitTime(_minimumWaitTime);
|
|
transferOwnership(address(_governor));
|
|
}
|
|
|
|
/**
|
|
* @notice Propose an emergency admin action to execute on the DVM as a set of proposed transactions.
|
|
* @dev Caller of this method must approve (and have) quorum amount of token to be pulled from their wallet.
|
|
* @param transactions array of transactions to be executed in the emergency action. When executed, will be sent
|
|
* via the governor contract.
|
|
* @return uint256 the emergency proposal id.
|
|
*/
|
|
function emergencyPropose(GovernorV2.Transaction[] memory transactions) external nonReentrant() returns (uint256) {
|
|
require(msg.sender != address(governor), "Governor can't propose"); // The governor should never be the proposer.
|
|
require(transactions.length > 0, "No transactions to propose");
|
|
token.safeTransferFrom(msg.sender, address(this), quorum);
|
|
uint256 id = emergencyProposals.length;
|
|
EmergencyProposal storage proposal = emergencyProposals.push();
|
|
proposal.sender = msg.sender;
|
|
proposal.lockedTokens = quorum;
|
|
proposal.expiryTime = uint64(getCurrentTime()) + minimumWaitTime;
|
|
|
|
for (uint256 i = 0; i < transactions.length; i++) proposal.transactions.push(transactions[i]);
|
|
|
|
emit EmergencyTransactionsProposed(id, msg.sender, msg.sender, proposal.expiryTime, quorum, transactions);
|
|
return id;
|
|
}
|
|
|
|
/**
|
|
* @notice After the proposal is executable, the executor or owner can use this function to remove the proposal
|
|
* without slashing.
|
|
* @dev This means that the DVM didn't explicitly reject the proposal. Allowing the executor to slash the quorum
|
|
* would give the executor too much power. So the only control either party has is to remove the proposal,
|
|
* releasing the bond. The proposal should not be removable before its liveness/expiry to ensure the regular Voting
|
|
* system's slash cannot be frontrun.
|
|
* @param id id of the proposal.
|
|
*/
|
|
function removeProposal(uint256 id) external nonReentrant() {
|
|
EmergencyProposal storage proposal = emergencyProposals[id];
|
|
require(proposal.expiryTime <= getCurrentTime(), "must be expired to remove");
|
|
require(msg.sender == owner() || msg.sender == executor, "owner or executor");
|
|
require(proposal.lockedTokens != 0, "invalid proposal");
|
|
token.safeTransfer(proposal.sender, proposal.lockedTokens);
|
|
emit EmergencyProposalRemoved(
|
|
id,
|
|
proposal.sender,
|
|
msg.sender,
|
|
proposal.expiryTime,
|
|
proposal.lockedTokens,
|
|
proposal.transactions
|
|
);
|
|
delete emergencyProposals[id];
|
|
}
|
|
|
|
/**
|
|
* @notice Before a proposal expires (or after), this method can be used by the owner, which should generally be
|
|
* the GovernorV2 contract, to slash the proposer.
|
|
* @dev The slash results in the proposer's tokens being sent to the Governor contract.
|
|
* @param id id of the proposal.
|
|
*/
|
|
function slashProposal(uint256 id) external nonReentrant() onlyOwner() {
|
|
EmergencyProposal storage proposal = emergencyProposals[id];
|
|
require(proposal.lockedTokens != 0, "invalid proposal");
|
|
token.safeTransfer(address(governor), proposal.lockedTokens);
|
|
emit EmergencyProposalSlashed(
|
|
id,
|
|
proposal.sender,
|
|
msg.sender,
|
|
proposal.expiryTime,
|
|
proposal.lockedTokens,
|
|
proposal.transactions
|
|
);
|
|
delete emergencyProposals[id];
|
|
}
|
|
|
|
/**
|
|
* @notice After a proposal expires, this method can be used by the executor to execute the proposal.
|
|
* @dev This method effectively gives the executor veto power over any proposal.
|
|
* @dev The first transaction execution sends the total amount of ETH required to complete all payable
|
|
* transactions in the Governor. The EmergencyProposer must receive this amount of ETH in advance.
|
|
* The executed transactions are then able to use this ETH by including a nonzero value.
|
|
* @param id id of the proposal.
|
|
*/
|
|
function executeEmergencyProposal(uint256 id) external payable nonReentrant() {
|
|
require(msg.sender == executor, "must be called by executor");
|
|
|
|
EmergencyProposal storage proposal = emergencyProposals[id];
|
|
require(proposal.lockedTokens != 0, "invalid proposal");
|
|
require(proposal.expiryTime <= getCurrentTime(), "must be expired to execute");
|
|
|
|
for (uint256 i = 0; i < proposal.transactions.length; i++)
|
|
governor.emergencyExecute{ value: address(this).balance }(proposal.transactions[i]);
|
|
|
|
token.safeTransfer(proposal.sender, proposal.lockedTokens);
|
|
emit EmergencyProposalExecuted(
|
|
id,
|
|
proposal.sender,
|
|
msg.sender,
|
|
proposal.expiryTime,
|
|
proposal.lockedTokens,
|
|
proposal.transactions
|
|
);
|
|
delete emergencyProposals[id];
|
|
}
|
|
|
|
/**
|
|
* @notice Admin method to set the quorum (bond) size.
|
|
* @dev Admin is intended to be the governance system.
|
|
* @param newQuorum the new quorum.
|
|
*/
|
|
function setQuorum(uint256 newQuorum) public nonReentrant() onlyOwner() {
|
|
require(newQuorum != 0, "quorum must be > 0");
|
|
require(newQuorum < token.totalSupply(), "quorum must be < totalSupply");
|
|
quorum = newQuorum;
|
|
emit QuorumSet(newQuorum);
|
|
}
|
|
|
|
/**
|
|
* @notice Admin method to set the executor address.
|
|
* @dev Admin is intended to be the governance system.
|
|
* @param newExecutor the new executor address.
|
|
*/
|
|
function setExecutor(address newExecutor) public nonReentrant() onlyOwner() {
|
|
executor = newExecutor;
|
|
emit ExecutorSet(newExecutor);
|
|
}
|
|
|
|
/**
|
|
* @notice Admin method to set the minimum wait time for a proposal to be executed.
|
|
* @dev Admin is intended to be the governance system. The minimum wait time is added to the current time at the
|
|
* time of the proposal to determine when the proposal will be executable. Any changes to this value after that
|
|
* point will have no impact on the proposal.
|
|
* @param newMinimumWaitTime the new minimum wait time.
|
|
*/
|
|
function setMinimumWaitTime(uint64 newMinimumWaitTime) public nonReentrant() onlyOwner() {
|
|
require(newMinimumWaitTime != 0, "minimumWaitTime == 0");
|
|
require(newMinimumWaitTime <= 4 weeks, "minimumWaitTime > 1 month");
|
|
minimumWaitTime = newMinimumWaitTime;
|
|
emit MinimumWaitTimeSet(newMinimumWaitTime);
|
|
}
|
|
|
|
/**
|
|
* @notice Returns the current block timestamp.
|
|
* @dev Can be overridden to control contract time.
|
|
* @return the current block timestamp.
|
|
*/
|
|
function getCurrentTime() public view virtual returns (uint256) {
|
|
return block.timestamp;
|
|
}
|
|
}
|