Skip to content

Commit

Permalink
Global optimistic auction rebalance extension (#154)
Browse files Browse the repository at this point in the history
* feat: GlobalAuctionRebalanceExtension contract, tests & utils

* test: adds cases that increase coverage to 100%

* feat: prevent intialization when not ready.

* chore(deps): add @uma/core as devDep

* ref(AuctionRebalance): startRebalance overridable

* ref(OptimisticAuction): adds Initial contract.

* feat: setProductSettings, proposeRebalance, override startRebalance.

* chore(deps): remove @uma/core dev dep.

* test: derive unit test template for GlobalOptimisticAuctionRebalanceExtension from GlobalAuctionRebalanceExtension.

* test: update deployment helper and barrel.

* refactor: remove unused param from constructor.

* chore(deps): add base58 encode/decode lib.

* feat: add OOV3 mock.

* test: extends to cover propose rebalance path.

* refactor: add events, update docstrings.

 fix assertedProducts update.

* feat: emit events, simplify tracking assertion id relationships, refactor out bonds.

* docs: update contract natspec description.

* fix: add virtual keyword back and removed extra imports in utils.

* ref: refactor and doc updates.
  • Loading branch information
snake-poison authored Nov 23, 2023
1 parent ec7ce83 commit d5ab030
Show file tree
Hide file tree
Showing 11 changed files with 1,483 additions and 6 deletions.
11 changes: 6 additions & 5 deletions contracts/global-extensions/GlobalAuctionRebalanceExtension.sol
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ contract GlobalAuctionRebalanceExtension is BaseGlobalExtension {


/* ============ Constructor ============ */
/*
* Instantiate with ManagerCore address and WrapModuleV2 address.
*
* @param _managerCore Address of ManagerCore contract
* @param _auctionModule Address of AuctionRebalanceModuleV1 contract
/**
* @dev Instantiate with ManagerCore address and WrapModuleV2 address.
*
* @param _managerCore Address of ManagerCore contract
* @param _auctionModule Address of AuctionRebalanceModuleV1 contract
*/
constructor(IManagerCore _managerCore, IAuctionRebalanceModuleV1 _auctionModule) public BaseGlobalExtension(_managerCore) {
auctionModule = _auctionModule;
Expand Down Expand Up @@ -155,6 +155,7 @@ contract GlobalAuctionRebalanceExtension is BaseGlobalExtension {
uint256 _positionMultiplier
)
external
virtual
onlyOperator(_setToken)
{
address[] memory currentComponents = _setToken.getComponents();
Expand Down

Large diffs are not rendered by default.

170 changes: 170 additions & 0 deletions contracts/interfaces/OptimisticOracleV3Interface.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.6.10;
pragma experimental "ABIEncoderV2";

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/**
* @title Optimistic Oracle V3 Interface that callers must use to assert truths about the world.
*/
interface OptimisticOracleV3Interface {
// Struct grouping together the settings related to the escalation manager stored in the assertion.
struct EscalationManagerSettings {
bool arbitrateViaEscalationManager; // False if the DVM is used as an oracle (EscalationManager on True).
bool discardOracle; // False if Oracle result is used for resolving assertion after dispute.
bool validateDisputers; // True if the EM isDisputeAllowed should be checked on disputes.
address assertingCaller; // Stores msg.sender when assertion was made.
address escalationManager; // Address of the escalation manager (zero address if not configured).
}

// Struct for storing properties and lifecycle of an assertion.
struct Assertion {
EscalationManagerSettings escalationManagerSettings; // Settings related to the escalation manager.
address asserter; // Address of the asserter.
uint64 assertionTime; // Time of the assertion.
bool settled; // True if the request is settled.
IERC20 currency; // ERC20 token used to pay rewards and fees.
uint64 expirationTime; // Unix timestamp marking threshold when the assertion can no longer be disputed.
bool settlementResolution; // Resolution of the assertion (false till resolved).
bytes32 domainId; // Optional domain that can be used to relate the assertion to others in the escalationManager.
bytes32 identifier; // UMA DVM identifier to use for price requests in the event of a dispute.
uint256 bond; // Amount of currency that the asserter has bonded.
address callbackRecipient; // Address that receives the callback.
address disputer; // Address of the disputer.
}

// Struct for storing cached currency whitelist.
struct WhitelistedCurrency {
bool isWhitelisted; // True if the currency is whitelisted.
uint256 finalFee; // Final fee of the currency.
}

/**
* @notice Returns the default identifier used by the Optimistic Oracle V3.
* @return The default identifier.
*/
function defaultIdentifier() external view returns (bytes32);

/**
* @notice Fetches information about a specific assertion and returns it.
* @param assertionId unique identifier for the assertion to fetch information for.
* @return assertion information about the assertion.
*/
function getAssertion(bytes32 assertionId) external view returns (Assertion memory);

/**
* @notice Asserts a truth about the world, using the default currency and liveness. No callback recipient or
* escalation manager is enabled. The caller is expected to provide a bond of finalFee/burnedBondPercentage
* (with burnedBondPercentage set to 50%, the bond is 2x final fee) of the default currency.
* @dev The caller must approve this contract to spend at least the result of getMinimumBond(defaultCurrency).
* @param claim the truth claim being asserted. This is an assertion about the world, and is verified by disputers.
* @param asserter receives bonds back at settlement. This could be msg.sender or
* any other account that the caller wants to receive the bond at settlement time.
* @return assertionId unique identifier for this assertion.
*/
function assertTruthWithDefaults(bytes memory claim, address asserter) external returns (bytes32);

/**
* @notice Asserts a truth about the world, using a fully custom configuration.
* @dev The caller must approve this contract to spend at least bond amount of currency.
* @param claim the truth claim being asserted. This is an assertion about the world, and is verified by disputers.
* @param asserter receives bonds back at settlement. This could be msg.sender or
* any other account that the caller wants to receive the bond at settlement time.
* @param callbackRecipient if configured, this address will receive a function call assertionResolvedCallback and
* assertionDisputedCallback at resolution or dispute respectively. Enables dynamic responses to these events. The
* recipient _must_ implement these callbacks and not revert or the assertion resolution will be blocked.
* @param escalationManager if configured, this address will control escalation properties of the assertion. This
* means a) choosing to arbitrate via the UMA DVM, b) choosing to discard assertions on dispute, or choosing to
* validate disputes. Combining these, the asserter can define their own security properties for the assertion.
* escalationManager also _must_ implement the same callbacks as callbackRecipient.
* @param liveness time to wait before the assertion can be resolved. Assertion can be disputed in this time.
* @param currency bond currency pulled from the caller and held in escrow until the assertion is resolved.
* @param bond amount of currency to pull from the caller and hold in escrow until the assertion is resolved. This
* must be >= getMinimumBond(address(currency)).
* @param identifier UMA DVM identifier to use for price requests in the event of a dispute. Must be pre-approved.
* @param domainId optional domain that can be used to relate this assertion to others in the escalationManager and
* can be used by the configured escalationManager to define custom behavior for groups of assertions. This is
* typically used for "escalation games" by changing bonds or other assertion properties based on the other
* assertions that have come before. If not needed this value should be 0 to save gas.
* @return assertionId unique identifier for this assertion.
*/
function assertTruth(
bytes memory claim,
address asserter,
address callbackRecipient,
address escalationManager,
uint64 liveness,
IERC20 currency,
uint256 bond,
bytes32 identifier,
bytes32 domainId
) external returns (bytes32);

/**
* @notice Fetches information about a specific identifier & currency from the UMA contracts and stores a local copy
* of the information within this contract. This is used to save gas when making assertions as we can avoid an
* external call to the UMA contracts to fetch this.
* @param identifier identifier to fetch information for and store locally.
* @param currency currency to fetch information for and store locally.
*/
function syncUmaParams(bytes32 identifier, address currency) external;

/**
* @notice Resolves an assertion. If the assertion has not been disputed, the assertion is resolved as true and the
* asserter receives the bond. If the assertion has been disputed, the assertion is resolved depending on the oracle
* result. Based on the result, the asserter or disputer receives the bond. If the assertion was disputed then an
* amount of the bond is sent to the UMA Store as an oracle fee based on the burnedBondPercentage. The remainder of
* the bond is returned to the asserter or disputer.
* @param assertionId unique identifier for the assertion to resolve.
*/
function settleAssertion(bytes32 assertionId) external;

/**
* @notice Settles an assertion and returns the resolution.
* @param assertionId unique identifier for the assertion to resolve and return the resolution for.
* @return resolution of the assertion.
*/
function settleAndGetAssertionResult(bytes32 assertionId) external returns (bool);

/**
* @notice Fetches the resolution of a specific assertion and returns it. If the assertion has not been settled then
* this will revert. If the assertion was disputed and configured to discard the oracle resolution return false.
* @param assertionId unique identifier for the assertion to fetch the resolution for.
* @return resolution of the assertion.
*/
function getAssertionResult(bytes32 assertionId) external view returns (bool);

/**
* @notice Returns the minimum bond amount required to make an assertion. This is calculated as the final fee of the
* currency divided by the burnedBondPercentage. If burn percentage is 50% then the min bond is 2x the final fee.
* @param currency currency to calculate the minimum bond for.
* @return minimum bond amount.
*/
function getMinimumBond(address currency) external view returns (uint256);

event AssertionMade(
bytes32 indexed assertionId,
bytes32 domainId,
bytes claim,
address indexed asserter,
address callbackRecipient,
address escalationManager,
address caller,
uint64 expirationTime,
IERC20 currency,
uint256 bond,
bytes32 indexed identifier
);

event AssertionDisputed(bytes32 indexed assertionId, address indexed caller, address indexed disputer);

event AssertionSettled(
bytes32 indexed assertionId,
address indexed bondRecipient,
bool disputed,
bool settlementResolution,
address settleCaller
);

event AdminPropertiesSet(IERC20 defaultCurrency, uint64 defaultLiveness, uint256 burnedBondPercentage);
}
142 changes: 142 additions & 0 deletions contracts/lib/AncillaryData.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.6.10;

/**
* @title Library for encoding and decoding ancillary data for DVM price requests.
* @notice We assume that on-chain ancillary data can be formatted directly from bytes to utf8 encoding via
* web3.utils.hexToUtf8, and that clients will parse the utf8-encoded ancillary data as a comma-delimitted key-value
* dictionary. Therefore, this library provides internal methods that aid appending to ancillary data from Solidity
* smart contracts. More details on UMA's ancillary data guidelines below:
* https://docs.google.com/document/d/1zhKKjgY1BupBGPPrY_WOJvui0B6DMcd-xDR8-9-SPDw/edit
*/
library AncillaryData {
// This converts the bottom half of a bytes32 input to hex in a highly gas-optimized way.
// Source: the brilliant implementation at https://gitter.im/ethereum/solidity?at=5840d23416207f7b0ed08c9b.
function toUtf8Bytes32Bottom(bytes32 bytesIn) private pure returns (bytes32) {
uint256 x = uint256(bytesIn);

// Nibble interleave
x = x & 0x00000000000000000000000000000000ffffffffffffffffffffffffffffffff;
x = (x | (x * 2**64)) & 0x0000000000000000ffffffffffffffff0000000000000000ffffffffffffffff;
x = (x | (x * 2**32)) & 0x00000000ffffffff00000000ffffffff00000000ffffffff00000000ffffffff;
x = (x | (x * 2**16)) & 0x0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff;
x = (x | (x * 2**8)) & 0x00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff;
x = (x | (x * 2**4)) & 0x0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f;

// Hex encode
uint256 h = (x & 0x0808080808080808080808080808080808080808080808080808080808080808) / 8;
uint256 i = (x & 0x0404040404040404040404040404040404040404040404040404040404040404) / 4;
uint256 j = (x & 0x0202020202020202020202020202020202020202020202020202020202020202) / 2;
x = x + (h & (i | j)) * 0x27 + 0x3030303030303030303030303030303030303030303030303030303030303030;

// Return the result.
return bytes32(x);
}

/**
* @notice Returns utf8-encoded bytes32 string that can be read via web3.utils.hexToUtf8.
* @dev Will return bytes32 in all lower case hex characters and without the leading 0x.
* This has minor changes from the toUtf8BytesAddress to control for the size of the input.
* @param bytesIn bytes32 to encode.
* @return utf8 encoded bytes32.
*/
function toUtf8Bytes(bytes32 bytesIn) internal pure returns (bytes memory) {
return abi.encodePacked(toUtf8Bytes32Bottom(bytesIn >> 128), toUtf8Bytes32Bottom(bytesIn));
}

/**
* @notice Returns utf8-encoded address that can be read via web3.utils.hexToUtf8.
* Source: https://ethereum.stackexchange.com/questions/8346/convert-address-to-string/8447#8447
* @dev Will return address in all lower case characters and without the leading 0x.
* @param x address to encode.
* @return utf8 encoded address bytes.
*/
function toUtf8BytesAddress(address x) internal pure returns (bytes memory) {
return
abi.encodePacked(toUtf8Bytes32Bottom(bytes32(bytes20(x)) >> 128), bytes8(toUtf8Bytes32Bottom(bytes20(x))));
}

/**
* @notice Converts a uint into a base-10, UTF-8 representation stored in a `string` type.
* @dev This method is based off of this code: https://stackoverflow.com/a/65707309.
*/
function toUtf8BytesUint(uint256 x) internal pure returns (bytes memory) {
if (x == 0) {
return "0";
}
uint256 j = x;
uint256 len;
while (j != 0) {
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint256 k = len;
while (x != 0) {
k = k - 1;
uint8 temp = (48 + uint8(x - (x / 10) * 10));
bytes1 b1 = bytes1(temp);
bstr[k] = b1;
x /= 10;
}
return bstr;
}

function appendKeyValueBytes32(
bytes memory currentAncillaryData,
bytes memory key,
bytes32 value
) internal pure returns (bytes memory) {
bytes memory prefix = constructPrefix(currentAncillaryData, key);
return abi.encodePacked(currentAncillaryData, prefix, toUtf8Bytes(value));
}

/**
* @notice Adds "key:value" to `currentAncillaryData` where `value` is an address that first needs to be converted
* to utf8 bytes. For example, if `utf8(currentAncillaryData)="k1:v1"`, then this function will return
* `utf8(k1:v1,key:value)`, and if `currentAncillaryData` is blank, then this will return `utf8(key:value)`.
* @param currentAncillaryData This bytes data should ideally be able to be utf8-decoded, but its OK if not.
* @param key Again, this bytes data should ideally be able to be utf8-decoded, but its OK if not.
* @param value An address to set as the value in the key:value pair to append to `currentAncillaryData`.
* @return Newly appended ancillary data.
*/
function appendKeyValueAddress(
bytes memory currentAncillaryData,
bytes memory key,
address value
) internal pure returns (bytes memory) {
bytes memory prefix = constructPrefix(currentAncillaryData, key);
return abi.encodePacked(currentAncillaryData, prefix, toUtf8BytesAddress(value));
}

/**
* @notice Adds "key:value" to `currentAncillaryData` where `value` is a uint that first needs to be converted
* to utf8 bytes. For example, if `utf8(currentAncillaryData)="k1:v1"`, then this function will return
* `utf8(k1:v1,key:value)`, and if `currentAncillaryData` is blank, then this will return `utf8(key:value)`.
* @param currentAncillaryData This bytes data should ideally be able to be utf8-decoded, but its OK if not.
* @param key Again, this bytes data should ideally be able to be utf8-decoded, but its OK if not.
* @param value A uint to set as the value in the key:value pair to append to `currentAncillaryData`.
* @return Newly appended ancillary data.
*/
function appendKeyValueUint(
bytes memory currentAncillaryData,
bytes memory key,
uint256 value
) internal pure returns (bytes memory) {
bytes memory prefix = constructPrefix(currentAncillaryData, key);
return abi.encodePacked(currentAncillaryData, prefix, toUtf8BytesUint(value));
}

/**
* @notice Helper method that returns the left hand side of a "key:value" pair plus the colon ":" and a leading
* comma "," if the `currentAncillaryData` is not empty. The return value is intended to be prepended as a prefix to
* some utf8 value that is ultimately added to a comma-delimited, key-value dictionary.
*/
function constructPrefix(bytes memory currentAncillaryData, bytes memory key) internal pure returns (bytes memory) {
if (currentAncillaryData.length > 0) {
return abi.encodePacked(",", key, ":");
} else {
return abi.encodePacked(key, ":");
}
}
}
Loading

0 comments on commit d5ab030

Please sign in to comment.