diff --git a/contracts/adapters/OptimisticAuctionRebalanceExtensionV1.sol b/contracts/adapters/OptimisticAuctionRebalanceExtensionV1.sol index d5221f15..447a69d9 100644 --- a/contracts/adapters/OptimisticAuctionRebalanceExtensionV1.sol +++ b/contracts/adapters/OptimisticAuctionRebalanceExtensionV1.sol @@ -20,23 +20,22 @@ pragma solidity 0.6.10; pragma experimental "ABIEncoderV2"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; import { AddressArrayUtils } from "../lib/AddressArrayUtils.sol"; - +import { AncillaryData } from "../lib/AncillaryData.sol"; +import { AssetAllowList } from "../lib/AssetAllowList.sol"; +import { AuctionRebalanceExtension } from "./AuctionRebalanceExtension.sol"; import { IAuctionRebalanceModuleV1 } from "../interfaces/IAuctionRebalanceModuleV1.sol"; import { IBaseManager } from "../interfaces/IBaseManager.sol"; import { ISetToken } from "../interfaces/ISetToken.sol"; -import {AuctionRebalanceExtension} from "./AuctionRebalanceExtension.sol"; -import {AncillaryData } from "../lib/AncillaryData.sol"; -import { AssetAllowList } from "../lib/AssetAllowList.sol"; -import {OptimisticOracleV3Interface} from "../interfaces/OptimisticOracleV3Interface.sol"; +import { OptimisticOracleV3Interface } from "../interfaces/OptimisticOracleV3Interface.sol"; /** - * @title BaseOptimisticAuctionRebalanceExtension + * @title OptimisticAuctionRebalanceExtension * @author Index Coop * - * @dev The contract extends `BaseAuctionRebalanceExtension` by adding an optimistic oracle mechanism for validating rules on the proposing and executing of rebalances. + * @dev The contract extends `AuctionRebalanceExtension` by adding an optimistic oracle mechanism for validating rules on the proposing and executing of rebalances. * It allows setting product-specific parameters for optimistic rebalancing and includes callback functions for resolved or disputed assertions. * @dev Version 1 is characterised by: Optional Asset Whitelist, No Set Token locking, control over rebalance timing via "isOpen" flag */ @@ -52,6 +51,7 @@ contract OptimisticAuctionRebalanceExtensionV1 is AuctionRebalanceExtension, As OptimisticRebalanceParams optimisticParams, bytes32 indexed rulesHash ); + event RebalanceProposed( ISetToken indexed setToken, IERC20 indexed quoteAsset, @@ -74,7 +74,11 @@ contract OptimisticAuctionRebalanceExtensionV1 is AuctionRebalanceExtension, As event ProposalDeleted( bytes32 assertionID, Proposal indexed proposal - ); + ); + + event IsOpenUpdated( + bool indexed isOpen + ); /* ============ Structs ============ */ @@ -114,16 +118,20 @@ contract OptimisticAuctionRebalanceExtensionV1 is AuctionRebalanceExtension, As bytes public constant PROPOSAL_HASH_KEY = "proposalHash"; bytes public constant RULES_KEY = "rulesIPFSHash"; - /* ============ Constructor ============ */ + /* - * @dev Initializes the BaseOptimisticAuctionRebalanceExtension with the passed parameters. + * @dev Initializes the OptimisticAuctionRebalanceExtension with the passed parameters. * * @param _auctionParams AuctionExtensionParams struct containing the baseManager and auctionModule addresses. */ - constructor(AuctionExtensionParams memory _auctionParams) public AuctionRebalanceExtension(_auctionParams.baseManager, _auctionParams.auctionModule) AssetAllowList(_auctionParams.allowedAssets, _auctionParams.useAssetAllowlist) { - - } + constructor( + AuctionExtensionParams memory _auctionParams + ) + public + AuctionRebalanceExtension(_auctionParams.baseManager, _auctionParams.auctionModule) + AssetAllowList(_auctionParams.allowedAssets, _auctionParams.useAssetAllowlist) + { } /* ============ Modifier ============ */ @@ -135,7 +143,7 @@ contract OptimisticAuctionRebalanceExtensionV1 is AuctionRebalanceExtension, As /* ============ External Functions ============ */ /** - * ONLY OPERATOR: Add new asset(s) that can be traded to, wrapped to, or claimed + * ONLY OPERATOR: Add new asset(s) that can be included as new components in rebalances * * @param _assets New asset(s) to add */ @@ -144,7 +152,7 @@ contract OptimisticAuctionRebalanceExtensionV1 is AuctionRebalanceExtension, As } /** - * ONLY OPERATOR: Remove asset(s) so that it/they can't be traded to, wrapped to, or claimed + * ONLY OPERATOR: Remove asset(s) so that it/they can't be included as new components in rebalances * * @param _assets Asset(s) to remove */ @@ -163,22 +171,20 @@ contract OptimisticAuctionRebalanceExtensionV1 is AuctionRebalanceExtension, As } /** - * ONLY OPERATOR: Toggle isOpen on and off. When false the extension is closed for proposing rebalances. - * when true it is open. - * - * @param _isOpen Bool indicating whether the extension is open for proposing rebalances. - */ + * ONLY OPERATOR: Toggle isOpen on and off. When false the extension is closed for proposing rebalances. + * when true it is open. + * + * @param _isOpen Bool indicating whether the extension is open for proposing rebalances. + */ function updateIsOpen(bool _isOpen) external onlyOperator { - isOpen = _isOpen; + _updateIsOpen(_isOpen); } - - /** - * @dev OPERATOR ONLY: sets product settings for a given set token - * @param _optimisticParams OptimisticRebalanceParams struct containing optimistic rebalance parameters. - * @param _rulesHash bytes32 containing the ipfs hash rules for the product. - */ + * @dev OPERATOR ONLY: sets product settings for a given set token + * @param _optimisticParams OptimisticRebalanceParams struct containing optimistic rebalance parameters. + * @param _rulesHash bytes32 containing the ipfs hash rules for the product. + */ function setProductSettings( OptimisticRebalanceParams memory _optimisticParams, bytes32 _rulesHash @@ -190,10 +196,13 @@ contract OptimisticAuctionRebalanceExtensionV1 is AuctionRebalanceExtension, As optimisticParams: _optimisticParams, rulesHash: _rulesHash }); + emit ProductSettingsUpdated(setToken, setToken.manager(), _optimisticParams, _rulesHash); } - /** + /** + * @dev IF OPEN ONLY: Proposes a rebalance for the SetToken using the Optimistic Oracle V3. + * * @param _quoteAsset ERC20 token used as the quote asset in auctions. * @param _oldComponents Addresses of existing components in the SetToken. * @param _newComponents Addresses of new components to be added. @@ -202,7 +211,7 @@ contract OptimisticAuctionRebalanceExtensionV1 is AuctionRebalanceExtension, As * the current component positions. Set to 0 for components being removed. * @param _rebalanceDuration Duration of the rebalance in seconds. * @param _positionMultiplier Position multiplier at the time target units were calculated. - */ + */ function proposeRebalance( IERC20 _quoteAsset, address[] memory _oldComponents, @@ -231,7 +240,6 @@ contract OptimisticAuctionRebalanceExtensionV1 is AuctionRebalanceExtension, As require(productSettings.rulesHash != bytes32(""), "Rules not set"); require(address(productSettings.optimisticParams.optimisticOracleV3) != address(0), "Oracle not set"); - bytes memory claim = _constructClaim(proposalHash, productSettings.rulesHash); uint256 totalBond = _pullBond(productSettings.optimisticParams); @@ -255,7 +263,6 @@ contract OptimisticAuctionRebalanceExtensionV1 is AuctionRebalanceExtension, As emit RebalanceProposed( setToken, _quoteAsset, _oldComponents, _newComponents, _newComponentsAuctionParams, _oldComponentsAuctionParams, _rebalanceDuration, _positionMultiplier); emit AssertedClaim(setToken, msg.sender, productSettings.rulesHash, assertionId, claim); - } /** @@ -332,20 +339,7 @@ contract OptimisticAuctionRebalanceExtensionV1 is AuctionRebalanceExtension, As ); invokeManager(address(auctionModule), callData); - isOpen = false; - } - - // Constructs the claim that will be asserted at the Optimistic Oracle V3. - function _constructClaim(bytes32 proposalHash, bytes32 rulesHash) internal pure returns (bytes memory) { - return - abi.encodePacked( - AncillaryData.appendKeyValueBytes32("", PROPOSAL_HASH_KEY, proposalHash), - ",", - RULES_KEY, - ":\"", - rulesHash, - "\"" - ); + _updateIsOpen(false); } /** @@ -380,6 +374,30 @@ contract OptimisticAuctionRebalanceExtensionV1 is AuctionRebalanceExtension, As emit ProposalDeleted(_assertionId, proposal); } + /* ============ Internal Functions ============ */ + + // Constructs the claim that will be asserted at the Optimistic Oracle V3. + function _constructClaim(bytes32 proposalHash, bytes32 rulesHash) internal pure returns (bytes memory) { + return + abi.encodePacked( + AncillaryData.appendKeyValueBytes32("", PROPOSAL_HASH_KEY, proposalHash), + ",", + RULES_KEY, + ":\"", + rulesHash, + "\"" + ); + } + + /// @notice Delete an existing proposal and associated assertionId. + /// @dev Internal function that deletes a proposal and associated assertionId. + /// @param assertionId assertionId of the proposal to delete. + function _deleteProposal(bytes32 assertionId) internal { + Proposal memory proposal = proposedProduct[assertionId]; + delete assertionIds[proposal.proposalHash]; + delete proposedProduct[assertionId]; + } + /// @notice Pulls the higher of the minimum bond or configured bond amount from the sender. /// @dev Internal function to pull the user's bond before asserting a claim. /// @param optimisticRebalanceParams optimistic rebalance parameters for the product. @@ -394,13 +412,8 @@ contract OptimisticAuctionRebalanceExtensionV1 is AuctionRebalanceExtension, As return totalBond; } - /// @notice Delete an existing proposal and associated assertionId. - /// @dev Internal function that deletes a proposal and associated assertionId. - /// @param assertionId assertionId of the proposal to delete. - function _deleteProposal(bytes32 assertionId) internal { - Proposal memory proposal = proposedProduct[assertionId]; - delete assertionIds[proposal.proposalHash]; - delete proposedProduct[assertionId]; + function _updateIsOpen(bool _isOpen) internal { + isOpen = _isOpen; + emit IsOpenUpdated(_isOpen); } - } diff --git a/contracts/lib/AssetAllowList.sol b/contracts/lib/AssetAllowList.sol index 4cf56c7e..845f4e69 100644 --- a/contracts/lib/AssetAllowList.sol +++ b/contracts/lib/AssetAllowList.sol @@ -1,5 +1,5 @@ /* - Copyright 2020 Set Labs Inc. + Copyright 2023 Index Coop Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,17 +20,14 @@ pragma solidity 0.6.10; import { AddressArrayUtils } from "./AddressArrayUtils.sol"; /** - * @title ModuleBase - * @author Set Protocol - * - * Abstract class that houses common Module-related state and functions. - * - * CHANGELOG: - * - 4/21/21: Delegated modifier logic to internal helpers to reduce contract size + * @title AssetAllowList + * @author Index Coop * + * Abstract contract that allows inheriting contracts to restrict the assets that can be traded to, wrapped to, or claimed */ abstract contract AssetAllowList { using AddressArrayUtils for address[]; + /* ============ Events ============ */ event AllowedAssetAdded( @@ -61,7 +58,7 @@ abstract contract AssetAllowList { modifier onlyAllowedAssets(address[] memory _assets) { require( _areAllowedAssets(_assets), - "BaseOptimisticAuctionRebalanceExtension.onlyAllowedAssets: Invalid asset" + "Invalid asset" ); _; } @@ -148,4 +145,3 @@ abstract contract AssetAllowList { return true; } } - diff --git a/external/abi/set/BoundedStepwiseLinearPriceAdapter.json b/external/abi/set/BoundedStepwiseLinearPriceAdapter.json new file mode 100644 index 00000000..25087d2b --- /dev/null +++ b/external/abi/set/BoundedStepwiseLinearPriceAdapter.json @@ -0,0 +1,206 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "BoundedStepwiseLinearPriceAdapter", + "sourceName": "contracts/protocol/integration/auction-price/BoundedStepwiseLinearPriceAdapter.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "_initialPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_slope", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_bucketSize", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_maxPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_minPrice", + "type": "uint256" + } + ], + "name": "areParamsValid", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "pure", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "getDecodedData", + "outputs": [ + { + "internalType": "uint256", + "name": "initialPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "slope", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "bucketSize", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "isDecreasing", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "maxPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minPrice", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_initialPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_slope", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_bucketSize", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "_isDecreasing", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "_maxPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_minPrice", + "type": "uint256" + } + ], + "name": "getEncodedData", + "outputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "stateMutability": "pure", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_timeElapsed", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_priceAdapterConfigData", + "type": "bytes" + } + ], + "name": "getPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function", + "gas": "0xa7d8c0" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_priceAdapterConfigData", + "type": "bytes" + } + ], + "name": "isPriceAdapterConfigDataValid", + "outputs": [ + { + "internalType": "bool", + "name": "isValid", + "type": "bool" + } + ], + "stateMutability": "pure", + "type": "function", + "gas": "0xa7d8c0" + } + ], + "bytecode": "0x608060405234801561001057600080fd5b506106d0806100206000396000f3fe608060405234801561001057600080fd5b50600436106100575760003560e01c8063079f6bf41461005c578063104b9b641461008457806363dc2e6b146100a5578063bf160e2514610105578063d3516db614610118575b600080fd5b61006f61006a36600461040a565b61015a565b60405190151581526020015b60405180910390f35b610097610092366004610463565b61018f565b60405190815260200161007b565b6100f86100b33660046104ee565b604080516020810197909752868101959095526060860193909352901515608085015260a084015260c0808401919091528151808403909101815260e0909201905290565b60405161007b9190610541565b61006f61011336600461058f565b6102fa565b61012b61012636600461040a565b610336565b6040805196875260208701959095529385019290925215156060840152608083015260a082015260c00161007b565b60008060008060008061016c87610336565b955095505094509450945061018485858585856102fa565b979650505050505050565b60008060008060008060006101a388610336565b9550955095509550955095506101bc86868685856102fa565b6102265760405162461bcd60e51b815260206004820152603160248201527f426f756e64656453746570776973654c696e6561725072696365416461707465604482015270723a20496e76616c696420706172616d7360781b606482015260840160405180910390fd5b6000610232858c6105e0565b9050610240866000196105e0565b81111561026257836102525782610254565b815b9750505050505050506102f0565b600061026e8783610602565b905084156102b5578781111561028e5782985050505050505050506102f0565b6102a661029b828a61061f565b848110818618021890565b985050505050505050506102f0565b6102c18860001961061f565b8111156102d85783985050505050505050506102f0565b6102a66102e5828a610632565b858111818718021890565b9695505050505050565b6000808611801561030b5750600085115b80156103175750600084115b80156103235750828611155b80156102f0575050909310159392505050565b600080600080600080868060200190518101906103539190610645565b949c939b5091995097509550909350915050565b634e487b7160e01b600052604160045260246000fd5b600082601f83011261038e57600080fd5b813567ffffffffffffffff808211156103a9576103a9610367565b604051601f8301601f19908116603f011681019082821181831017156103d1576103d1610367565b816040528381528660208588010111156103ea57600080fd5b836020870160208301376000602085830101528094505050505092915050565b60006020828403121561041c57600080fd5b813567ffffffffffffffff81111561043357600080fd5b61043f8482850161037d565b949350505050565b80356001600160a01b038116811461045e57600080fd5b919050565b60008060008060008060c0878903121561047c57600080fd5b61048587610447565b955061049360208801610447565b945060408701359350606087013592506080870135915060a087013567ffffffffffffffff8111156104c457600080fd5b6104d089828a0161037d565b9150509295509295509295565b80151581146104eb57600080fd5b50565b60008060008060008060c0878903121561050757600080fd5b8635955060208701359450604087013593506060870135610527816104dd565b9598949750929560808101359460a0909101359350915050565b600060208083528351808285015260005b8181101561056e57858101830151858201604001528201610552565b506000604082860101526040601f19601f8301168501019250505092915050565b600080600080600060a086880312156105a757600080fd5b505083359560208501359550604085013594606081013594506080013592509050565b634e487b7160e01b600052601160045260246000fd5b6000826105fd57634e487b7160e01b600052601260045260246000fd5b500490565b8082028115828204841417610619576106196105ca565b92915050565b81810381811115610619576106196105ca565b80820180821115610619576106196105ca565b60008060008060008060c0878903121561065e57600080fd5b865195506020870151945060408701519350606087015161067e816104dd565b809350506080870151915060a08701519050929550929550929556fea2646970667358221220a50447f4d241ad3f0e8ac061ae0c04c474f1f7c3a7a5cab954e568dac556890e64736f6c63430008110033", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100575760003560e01c8063079f6bf41461005c578063104b9b641461008457806363dc2e6b146100a5578063bf160e2514610105578063d3516db614610118575b600080fd5b61006f61006a36600461040a565b61015a565b60405190151581526020015b60405180910390f35b610097610092366004610463565b61018f565b60405190815260200161007b565b6100f86100b33660046104ee565b604080516020810197909752868101959095526060860193909352901515608085015260a084015260c0808401919091528151808403909101815260e0909201905290565b60405161007b9190610541565b61006f61011336600461058f565b6102fa565b61012b61012636600461040a565b610336565b6040805196875260208701959095529385019290925215156060840152608083015260a082015260c00161007b565b60008060008060008061016c87610336565b955095505094509450945061018485858585856102fa565b979650505050505050565b60008060008060008060006101a388610336565b9550955095509550955095506101bc86868685856102fa565b6102265760405162461bcd60e51b815260206004820152603160248201527f426f756e64656453746570776973654c696e6561725072696365416461707465604482015270723a20496e76616c696420706172616d7360781b606482015260840160405180910390fd5b6000610232858c6105e0565b9050610240866000196105e0565b81111561026257836102525782610254565b815b9750505050505050506102f0565b600061026e8783610602565b905084156102b5578781111561028e5782985050505050505050506102f0565b6102a661029b828a61061f565b848110818618021890565b985050505050505050506102f0565b6102c18860001961061f565b8111156102d85783985050505050505050506102f0565b6102a66102e5828a610632565b858111818718021890565b9695505050505050565b6000808611801561030b5750600085115b80156103175750600084115b80156103235750828611155b80156102f0575050909310159392505050565b600080600080600080868060200190518101906103539190610645565b949c939b5091995097509550909350915050565b634e487b7160e01b600052604160045260246000fd5b600082601f83011261038e57600080fd5b813567ffffffffffffffff808211156103a9576103a9610367565b604051601f8301601f19908116603f011681019082821181831017156103d1576103d1610367565b816040528381528660208588010111156103ea57600080fd5b836020870160208301376000602085830101528094505050505092915050565b60006020828403121561041c57600080fd5b813567ffffffffffffffff81111561043357600080fd5b61043f8482850161037d565b949350505050565b80356001600160a01b038116811461045e57600080fd5b919050565b60008060008060008060c0878903121561047c57600080fd5b61048587610447565b955061049360208801610447565b945060408701359350606087013592506080870135915060a087013567ffffffffffffffff8111156104c457600080fd5b6104d089828a0161037d565b9150509295509295509295565b80151581146104eb57600080fd5b50565b60008060008060008060c0878903121561050757600080fd5b8635955060208701359450604087013593506060870135610527816104dd565b9598949750929560808101359460a0909101359350915050565b600060208083528351808285015260005b8181101561056e57858101830151858201604001528201610552565b506000604082860101526040601f19601f8301168501019250505092915050565b600080600080600060a086880312156105a757600080fd5b505083359560208501359550604085013594606081013594506080013592509050565b634e487b7160e01b600052601160045260246000fd5b6000826105fd57634e487b7160e01b600052601260045260246000fd5b500490565b8082028115828204841417610619576106196105ca565b92915050565b81810381811115610619576106196105ca565b80820180821115610619576106196105ca565b60008060008060008060c0878903121561065e57600080fd5b865195506020870151945060408701519350606087015161067e816104dd565b809350506080870151915060a08701519050929550929550929556fea2646970667358221220a50447f4d241ad3f0e8ac061ae0c04c474f1f7c3a7a5cab954e568dac556890e64736f6c63430008110033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/external/contracts/set/BoundedStepwiseLinearPriceAdapter.sol b/external/contracts/set/BoundedStepwiseLinearPriceAdapter.sol new file mode 100644 index 00000000..23be78fb --- /dev/null +++ b/external/contracts/set/BoundedStepwiseLinearPriceAdapter.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.17; + +import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol"; + +/** + * @title BoundedStepwiseLinearPriceAdapter + * @author Index Coop + * @notice Price adapter contract for the AuctionRebalanceModuleV1. It returns a price that + * increases or decreases linearly in steps over time, within a bounded range. + * The rate of change is constant. + * Price formula: price = initialPrice +/- slope * timeBucket + */ +contract BoundedStepwiseLinearPriceAdapter { + + /** + * @dev Calculates and returns the linear price. + * + * @param _timeElapsed Time elapsed since the start of the auction. + * @param _priceAdapterConfigData Encoded bytes representing the linear function parameters. + * + * @return price The price calculated using the linear function. + */ + function getPrice( + address /* _setToken */, + address /* _component */, + uint256 /* _componentQuantity */, + uint256 _timeElapsed, + uint256 /* _duration */, + bytes memory _priceAdapterConfigData + ) + external + pure + returns (uint256 price) + { + ( + uint256 initialPrice, + uint256 slope, + uint256 bucketSize, + bool isDecreasing, + uint256 maxPrice, + uint256 minPrice + ) = getDecodedData(_priceAdapterConfigData); + + require( + areParamsValid(initialPrice, slope, bucketSize, maxPrice, minPrice), + "BoundedStepwiseLinearPriceAdapter: Invalid params" + ); + + uint256 bucket = _timeElapsed / bucketSize; + + // Protect against priceChange overflow + if (bucket > type(uint256).max / slope) { + return isDecreasing ? minPrice : maxPrice; + } + + uint256 priceChange = bucket * slope; + + if (isDecreasing) { + // Protect against price underflow + if (priceChange > initialPrice) { + return minPrice; + } + return FixedPointMathLib.max(initialPrice - priceChange, minPrice); + } else { + // Protect against price overflow + if (priceChange > type(uint256).max - initialPrice) { + return maxPrice; + } + return FixedPointMathLib.min(initialPrice + priceChange, maxPrice); + } + } + + /** + * @dev Returns true if the price adapter is valid for the given parameters. + * + * @param _priceAdapterConfigData Encoded data for configuring the price adapter. + * + * @return isValid Boolean indicating if the adapter config data is valid. + */ + function isPriceAdapterConfigDataValid( + bytes memory _priceAdapterConfigData + ) + external + pure + returns (bool isValid) + { + ( + uint256 initialPrice, + uint256 slope, + uint256 bucketSize, + , + uint256 maxPrice, + uint256 minPrice + ) = getDecodedData(_priceAdapterConfigData); + + return areParamsValid(initialPrice, slope, bucketSize, maxPrice, minPrice); + } + + /** + * @dev Returns true if the price adapter parameters are valid. + * + * @param _initialPrice Initial price of the auction + * @param _bucketSize Time elapsed between each bucket + * @param _maxPrice Maximum price of the auction + * @param _minPrice Minimum price of the auction + */ + function areParamsValid( + uint256 _initialPrice, + uint256 _slope, + uint256 _bucketSize, + uint256 _maxPrice, + uint256 _minPrice + ) + public + pure + returns (bool) + { + return _initialPrice > 0 + && _slope > 0 + && _bucketSize > 0 + && _initialPrice <= _maxPrice + && _initialPrice >= _minPrice; + } + + /** + * @dev Returns the encoded data for the price curve parameters + * + * @param _initialPrice Initial price of the auction + * @param _slope Slope of the linear price change + * @param _bucketSize Time elapsed between each bucket + * @param _isDecreasing Flag for whether the price is decreasing or increasing + * @param _maxPrice Maximum price of the auction + * @param _minPrice Minimum price of the auction + */ + function getEncodedData( + uint256 _initialPrice, + uint256 _slope, + uint256 _bucketSize, + bool _isDecreasing, + uint256 _maxPrice, + uint256 _minPrice + ) + external + pure + returns (bytes memory data) + { + return abi.encode(_initialPrice, _slope, _bucketSize, _isDecreasing, _maxPrice, _minPrice); + } + + /** + * @dev Decodes the parameters from the provided bytes. + * + * @param _data Bytes encoded auction parameters + * @return initialPrice Initial price of the auction + * @return slope Slope of the linear price change + * @return bucketSize Time elapsed between each bucket + * @return isDecreasing Flag for whether the price is decreasing or increasing + * @return maxPrice Maximum price of the auction + * @return minPrice Minimum price of the auction + */ + function getDecodedData(bytes memory _data) + public + pure + returns (uint256 initialPrice, uint256 slope, uint256 bucketSize, bool isDecreasing, uint256 maxPrice, uint256 minPrice) + { + return abi.decode(_data, (uint256, uint256, uint256, bool, uint256, uint256)); + } +} diff --git a/test/adapters/optimisticAuctionRebalanceExtensionV1.spec.ts b/test/adapters/optimisticAuctionRebalanceExtensionV1.spec.ts index f02627e8..08951ff9 100644 --- a/test/adapters/optimisticAuctionRebalanceExtensionV1.spec.ts +++ b/test/adapters/optimisticAuctionRebalanceExtensionV1.spec.ts @@ -1,6 +1,7 @@ import "module-alias/register"; import { Address, Account } from "@utils/types"; +import { base58ToHexString } from "@utils/common"; import { ADDRESS_ZERO, ZERO } from "@utils/constants"; import { BaseManager, @@ -23,29 +24,10 @@ import { getRandomAccount, } from "@utils/index"; import { SetFixture } from "@utils/fixtures"; -import { BigNumber, ContractTransaction, utils } from "ethers"; -import base58 from "bs58"; +import { BigNumber, ContractTransaction, utils, constants } from "ethers"; const expect = getWaffleExpect(); -function bufferToHex(buffer: Uint8Array) { - let hexStr = ""; - - for (let i = 0; i < buffer.length; i++) { - const hex = (buffer[i] & 0xff).toString(16); - hexStr += hex.length === 1 ? "0" + hex : hex; - } - - return hexStr; -} - -// Base58 decoding function (make sure you have a proper Base58 decoding function) -function base58ToHexString(base58String: string) { - const bytes = base58.decode(base58String); // Decode base58 to a buffer - const hexString = bufferToHex(bytes.slice(2)); // Convert buffer to hex, excluding the first 2 bytes - return "0x" + hexString; -} - describe("OptimisticAuctionRebalanceExtensionV1", () => { let owner: Account; let methodologist: Account; @@ -81,7 +63,7 @@ describe("OptimisticAuctionRebalanceExtensionV1", () => { priceAdapter = await deployer.setV2.deployConstantPriceAdapter(); optimisticOracleV3Mock = await deployer.mocks.deployOptimisticOracleV3Mock(); optimisticOracleV3MockUpgraded = await deployer.mocks.deployOptimisticOracleV3Mock(); - collateralAsset = await deployer.mocks.deployStandardTokenMock(owner.address, 18); + collateralAsset = await deployer.mocks.deployStandardTokenMock(operator.address, 18); await setV2Setup.integrationRegistry.addIntegration( setV2Setup.auctionModule.address, @@ -115,6 +97,9 @@ describe("OptimisticAuctionRebalanceExtensionV1", () => { allowedAssets, ); auctionRebalanceExtension = auctionRebalanceExtension.connect(operator.wallet); + await collateralAsset + .connect(operator.wallet) + .approve(auctionRebalanceExtension.address, ether(1000)); }); addSnapshotBeforeRestoreAfterEach(); @@ -141,7 +126,7 @@ describe("OptimisticAuctionRebalanceExtensionV1", () => { ); } - it("should set the correct manager core", async () => { + it("should set the correct base manager", async () => { const auctionExtension = await subject(); const actualBaseManager = await auctionExtension.manager(); @@ -179,7 +164,7 @@ describe("OptimisticAuctionRebalanceExtensionV1", () => { expect(isInitialized).to.be.true; }); - describe("when the initializer is not the owner", () => { + describe("when the initializer is not the operator", () => { beforeEach(async () => { subjectCaller = await getRandomAccount(); }); @@ -196,16 +181,22 @@ describe("OptimisticAuctionRebalanceExtensionV1", () => { }); context("when the product settings have been set", () => { + let rulesHash: Uint8Array; + let bondAmount: BigNumber; beforeEach(async () => { + rulesHash = utils.arrayify( + base58ToHexString("Qmc5gCcjYypU7y28oCALwfSvxCBskLuPKWpK4qpterKC7z"), + ); + bondAmount = ether(140); // 140 INDEX minimum bond await auctionRebalanceExtension.connect(operator.wallet).setProductSettings( { collateral: collateralAsset.address, - liveness: BigNumber.from(0), - bondAmount: BigNumber.from(0), + liveness: BigNumber.from(60 * 60), // 7 days + bondAmount, identifier: utils.formatBytes32String(""), optimisticOracleV3: optimisticOracleV3Mock.address, }, - utils.arrayify(base58ToHexString("Qmc5gCcjYypU7y28oCALwfSvxCBskLuPKWpK4qpterKC7z")), + rulesHash, ); }); @@ -291,13 +282,59 @@ describe("OptimisticAuctionRebalanceExtensionV1", () => { ); } + function constructClaim(): string { + const abi = utils.defaultAbiCoder; + const proposalHash = utils.keccak256( + abi.encode( + [ + "address", + "address", + "address[]", + "address[]", + "(uint256,string,bytes)[]", + "(uint256,string,bytes)[]", + "bool", + "uint256", + "uint256", + ], + [ + setToken.address, + subjectQuoteAsset, + subjectOldComponents, + subjectNewComponents, + subjectNewComponentsAuctionParams.map(component => [ + component.targetUnit, + component.priceAdapterName, + component.priceAdapterConfigData, + ]), + subjectOldComponentsAuctionParams.map(component => [ + component.targetUnit, + component.priceAdapterName, + component.priceAdapterConfigData, + ]), + false, // We don't allow locking the set token in this version + subjectRebalanceDuration, + subjectPositionMultiplier, + ], + ), + ); + const firstPart = utils.toUtf8Bytes( + "proposalHash:" + proposalHash.slice(2) + ',rulesIPFSHash:"', + ); + const lastPart = utils.toUtf8Bytes('"'); + + return utils.hexlify(utils.concat([firstPart, rulesHash, lastPart])); + } + context("when the extension is open for rebalance", () => { beforeEach(async () => { await auctionRebalanceExtension.updateIsOpen(true); }); + it("should not revert", async () => { await subject(); }); + it("should update proposed products correctly", async () => { await subject(); const proposal = await auctionRebalanceExtension @@ -305,16 +342,122 @@ describe("OptimisticAuctionRebalanceExtensionV1", () => { .proposedProduct(utils.formatBytes32String("win")); expect(proposal.product).to.eq(setToken.address); }); + + it("should pull bond", async () => { + const collateralBalanceBefore = await collateralAsset.balanceOf( + subjectCaller.address, + ); + await subject(); + const collateralBalanceAfter = await collateralAsset.balanceOf( + subjectCaller.address, + ); + expect(collateralBalanceAfter).to.eq(collateralBalanceBefore.sub(bondAmount)); + }); + + it("should emit RebalanceProposed event", async () => { + const receipt = (await subject().then(tx => tx.wait())) as any; + const proposeEvent = receipt.events.find( + (event: any) => event.event === "RebalanceProposed", + ); + expect(proposeEvent.args.setToken).to.eq(setToken.address); + expect(proposeEvent.args.quoteAsset).to.eq(subjectQuoteAsset); + expect(proposeEvent.args.oldComponents).to.deep.eq(subjectOldComponents); + expect(proposeEvent.args.newComponents).to.deep.eq(subjectNewComponents); + expect(proposeEvent.args.rebalanceDuration).to.eq(subjectRebalanceDuration); + expect(proposeEvent.args.positionMultiplier).to.eq(subjectPositionMultiplier); + + const newComponentsAuctionParams = proposeEvent.args.newComponentsAuctionParams.map( + (entry: any) => { + return { + priceAdapterConfigData: entry.priceAdapterConfigData, + priceAdapterName: entry.priceAdapterName, + targetUnit: entry.targetUnit, + }; + }, + ); + expect(newComponentsAuctionParams).to.deep.eq(subjectNewComponentsAuctionParams); + + const oldComponentsAuctionParams = proposeEvent.args.oldComponentsAuctionParams.map( + (entry: any) => { + return { + priceAdapterConfigData: entry.priceAdapterConfigData, + priceAdapterName: entry.priceAdapterName, + targetUnit: entry.targetUnit, + }; + }, + ); + expect(oldComponentsAuctionParams).to.deep.eq(subjectOldComponentsAuctionParams); + }); + + it("should emit AssertedClaim event", async () => { + const receipt = (await subject().then( tx => tx.wait())) as any; + const assertEvent = receipt.events.find( + (event: any) => event.event === "AssertedClaim", + ); + const emittedSetToken = assertEvent.args.setToken; + expect(emittedSetToken).to.eq(setToken.address); + const assertedBy = assertEvent.args._assertedBy; + expect(assertedBy).to.eq(operator.wallet.address); + const emittedRulesHash = assertEvent.args.rulesHash; + expect(emittedRulesHash).to.eq(utils.hexlify(rulesHash)); + const claim = assertEvent.args._claimData; + expect(claim).to.eq(constructClaim()); + }); + + context("when the same rebalance has been proposed already", () => { + beforeEach(async () => { + await subject(); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Proposal already exists"); + }); + }); + + context("when the rule hash is empty", () => { + beforeEach(async () => { + const currentSettings = await auctionRebalanceExtension.productSettings(); + await auctionRebalanceExtension.setProductSettings( + currentSettings.optimisticParams, + constants.HashZero, + ); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Rules not set"); + }); + }); + + context("when the oracle address is zero", () => { + beforeEach(async () => { + const [ + currentOptimisticParams, + ruleHash, + ] = await auctionRebalanceExtension.productSettings(); + const optimisticParams = { + ...currentOptimisticParams, + optimisticOracleV3: constants.AddressZero, + }; + await auctionRebalanceExtension.setProductSettings(optimisticParams, ruleHash); + }); + + it("should revert", async () => { + await expect(subject()).to.be.revertedWith("Oracle not set"); + }); + }); }); + context("when the extension is not open for rebalance", () => { beforeEach(async () => { await auctionRebalanceExtension.updateIsOpen(false); }); + it("should revert", async () => { expect(subject()).to.be.revertedWith("Must be open for rebalancing"); }); }); }); + describe("#startRebalance", () => { async function subject(): Promise { return auctionRebalanceExtension @@ -508,6 +651,7 @@ describe("OptimisticAuctionRebalanceExtensionV1", () => { .proposedProduct(utils.formatBytes32String("win")); expect(proposalAfter.product).to.eq(ADDRESS_ZERO); }); + it("should delete the proposal on a disputed callback from currently set oracle", async () => { await auctionRebalanceExtension.connect(operator.wallet).setProductSettings( { @@ -521,6 +665,7 @@ describe("OptimisticAuctionRebalanceExtensionV1", () => { base58ToHexString("Qmc5gCcjYypU7y28oCALwfSvxCBskLuPKWpK4qpterKC7z"), ), ); + const proposal = await auctionRebalanceExtension .connect(subjectCaller.wallet) .proposedProduct(utils.formatBytes32String("win")); @@ -533,12 +678,14 @@ describe("OptimisticAuctionRebalanceExtensionV1", () => { utils.formatBytes32String("win"), ), ).to.not.be.reverted; + const proposalAfter = await auctionRebalanceExtension .connect(subjectCaller.wallet) .proposedProduct(utils.formatBytes32String("win")); expect(proposalAfter.product).to.eq(ADDRESS_ZERO); }); }); + describe("assertionResolvedCallback", () => { it("should not revert on a resolved callback", async () => { await expect( @@ -614,6 +761,7 @@ describe("OptimisticAuctionRebalanceExtensionV1", () => { }); }); }); + describe("#setRaiseTargetPercentage", () => { let subjectRaiseTargetPercentage: BigNumber; let subjectCaller: Account; diff --git a/test/global-extensions/globalOptimisticAuctionRebalanceExtension.spec.ts b/test/global-extensions/globalOptimisticAuctionRebalanceExtension.spec.ts index d108c665..56dc8cdf 100644 --- a/test/global-extensions/globalOptimisticAuctionRebalanceExtension.spec.ts +++ b/test/global-extensions/globalOptimisticAuctionRebalanceExtension.spec.ts @@ -1,6 +1,7 @@ import "module-alias/register"; import { Address, Account } from "@utils/types"; +import { base58ToHexString } from "@utils/common"; import { ADDRESS_ZERO, ZERO } from "@utils/constants"; import { GlobalOptimisticAuctionRebalanceExtension, DelegatedManager, ManagerCore, @@ -21,29 +22,10 @@ import { } from "@utils/index"; import { SetFixture } from "@utils/fixtures"; import { BigNumber, ContractTransaction, utils } from "ethers"; -import base58 from "bs58"; const expect = getWaffleExpect(); -function bufferToHex(buffer: Uint8Array) { - let hexStr = ""; - - for (let i = 0; i < buffer.length; i++) { - const hex = (buffer[i] & 0xff).toString(16); - hexStr += (hex.length === 1) ? "0" + hex : hex; - } - - return hexStr; -} - -// Base58 decoding function (make sure you have a proper Base58 decoding function) -function base58ToHexString(base58String: string) { - const bytes = base58.decode(base58String); // Decode base58 to a buffer - const hexString = bufferToHex(bytes.slice(2)); // Convert buffer to hex, excluding the first 2 bytes - return "0x" + hexString; -} - -describe.only("GlobalOptimisticAuctionRebalanceExtension", () => { +describe("GlobalOptimisticAuctionRebalanceExtension", () => { let owner: Account; let methodologist: Account; let operator: Account; diff --git a/test/integration/ethereum/addresses.ts b/test/integration/ethereum/addresses.ts index 4e8bcf0e..a198db19 100644 --- a/test/integration/ethereum/addresses.ts +++ b/test/integration/ethereum/addresses.ts @@ -2,6 +2,7 @@ import structuredClone from "@ungap/structured-clone"; export const PRODUCTION_ADDRESSES = { tokens: { + index: "0x0954906da0Bf32d5479e25f46056d22f08464cab", stEthAm: "0x28424507fefb6f7f8E9D3860F56504E4e5f5f390", stEth: "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", dai: "0x6B175474E89094C44Da98b954EedeAC495271d0F", @@ -22,6 +23,8 @@ export const PRODUCTION_ADDRESSES = { rETH2: "0x20BC832ca081b91433ff6c17f85701B6e92486c5", sETH2: "0xFe2e637202056d30016725477c5da089Ab0A043A", wbtc: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + swETH: "0xf951E335afb289353dc249e82926178EaC7DEd78", + ETHx: "0xA35b1B31Ce002FBF2058D22F30f95D405200A15b", }, whales: { stEth: "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", @@ -72,6 +75,7 @@ export const PRODUCTION_ADDRESSES = { aaveV3LeverageStrategyExtension: "0x7d3f7EDD04916F3Cb2bC6740224c636B9AE43200", aaveV3LeverageModule: "0x71E932715F5987077ADC5A7aA245f38841E0DcBe", constantPriceAdapter: "0x13c33656570092555Bf27Bdf53Ce24482B85D992", + linearPriceAdapter: "0x237F7BBe0b358415bE84AB6d279D4338C0d026bB", }, lending: { aave: { diff --git a/test/integration/ethereum/optimisticAuctionRebalanceExtenisonV1.spec.ts b/test/integration/ethereum/optimisticAuctionRebalanceExtenisonV1.spec.ts index b2fc934d..08b570af 100644 --- a/test/integration/ethereum/optimisticAuctionRebalanceExtenisonV1.spec.ts +++ b/test/integration/ethereum/optimisticAuctionRebalanceExtenisonV1.spec.ts @@ -3,13 +3,14 @@ import "module-alias/register"; import { Address, Account } from "@utils/types"; import { increaseTimeAsync } from "@utils/test"; import { setBlockNumber } from "@utils/test/testingUtils"; -import { ADDRESS_ZERO, ZERO } from "@utils/constants"; +import { base58ToHexString } from "@utils/common"; +import { ADDRESS_ZERO, ONE_HOUR_IN_SECONDS, ZERO } from "@utils/constants"; import { OptimisticAuctionRebalanceExtensionV1 } from "@utils/contracts/index"; import { AuctionRebalanceModuleV1, AuctionRebalanceModuleV1__factory, - ConstantPriceAdapter, - ConstantPriceAdapter__factory, + BoundedStepwiseLinearPriceAdapter, + BoundedStepwiseLinearPriceAdapter__factory, SetToken, SetToken__factory, BaseManagerV2, @@ -18,8 +19,8 @@ import { IntegrationRegistry__factory, IIdentifierWhitelist, IIdentifierWhitelist__factory, - IWETH, - IWETH__factory, + IERC20, + IERC20__factory, OptimisticOracleV3Mock, OptimisticOracleV3Interface, OptimisticOracleV3Interface__factory, @@ -32,37 +33,21 @@ import { ether, getAccounts, getWaffleExpect, - usdc, getTransactionTimestamp, getRandomAccount, } from "@utils/index"; import { BigNumber, ContractTransaction, utils, Signer } from "ethers"; import { ethers } from "hardhat"; -import base58 from "bs58"; const expect = getWaffleExpect(); -function bufferToHex(buffer: Uint8Array) { - let hexStr = ""; - - for (let i = 0; i < buffer.length; i++) { - const hex = (buffer[i] & 0xff).toString(16); - hexStr += hex.length === 1 ? "0" + hex : hex; - } - - return hexStr; -} - -// Base58 decoding function (make sure you have a proper Base58 decoding function) -function base58ToHexString(base58String: string) { - const bytes = base58.decode(base58String); // Decode base58 to a buffer - const hexString = bufferToHex(bytes.slice(2)); // Convert buffer to hex, excluding the first 2 bytes - return "0x" + hexString; -} - if (process.env.INTEGRATIONTEST) { - describe("OptimisticAuctionRebalanceExtensionV1 - Integration Test dsEth", () => { + describe.only("OptimisticAuctionRebalanceExtensionV1 - Integration Test dsEth", () => { const contractAddresses = PRODUCTION_ADDRESSES; + + const liveness = BigNumber.from(60 * 60 * 24 * 2); // 2 days + const minimumBond = ether(140); // 140 INDEX Minimum Bond + let owner: Account; let methodologist: Account; let operator: Signer; @@ -75,7 +60,7 @@ if (process.env.INTEGRATIONTEST) { let auctionRebalanceExtension: OptimisticAuctionRebalanceExtensionV1; let integrationRegistry: IntegrationRegistry; - let priceAdapter: ConstantPriceAdapter; + let priceAdapter: BoundedStepwiseLinearPriceAdapter; // UMA contracts let optimisticOracleV3: OptimisticOracleV3Interface; @@ -87,22 +72,21 @@ if (process.env.INTEGRATIONTEST) { let useAssetAllowlist: boolean; let allowedAssets: Address[]; - let weth: IWETH; - let minimumBond: BigNumber; + let indexToken: IERC20; - setBlockNumber(18789000); + setBlockNumber(18924016); before(async () => { [owner, methodologist] = await getAccounts(); deployer = new DeployHelper(owner.wallet); - priceAdapter = ConstantPriceAdapter__factory.connect( - contractAddresses.setFork.constantPriceAdapter, + priceAdapter = BoundedStepwiseLinearPriceAdapter__factory.connect( + contractAddresses.setFork.linearPriceAdapter, owner.wallet, ); - weth = IWETH__factory.connect(contractAddresses.tokens.weth, owner.wallet); - collateralAssetAddress = weth.address; + indexToken = IERC20__factory.connect(contractAddresses.tokens.index, owner.wallet); + collateralAssetAddress = indexToken.address; optimisticOracleV3 = OptimisticOracleV3Interface__factory.connect( contractAddresses.oracles.uma.optimisticOracleV3, @@ -121,7 +105,6 @@ if (process.env.INTEGRATIONTEST) { ethers.utils.parseEther("10").toHexString(), ]); identifierWhitelist = identifierWhitelist.connect(whitelistOwner); - minimumBond = await optimisticOracleV3.getMinimumBond(collateralAssetAddress); integrationRegistry = IntegrationRegistry__factory.connect( contractAddresses.setFork.integrationRegistry, @@ -135,8 +118,8 @@ if (process.env.INTEGRATIONTEST) { owner.wallet, ); - useAssetAllowlist = false; - allowedAssets = []; + useAssetAllowlist = true; + allowedAssets = [contractAddresses.tokens.swETH, contractAddresses.tokens.ETHx]; // New dsETH components dsEth = SetToken__factory.connect(contractAddresses.tokens.dsEth, owner.wallet); @@ -153,6 +136,12 @@ if (process.env.INTEGRATIONTEST) { auctionRebalanceExtension = auctionRebalanceExtension.connect(operator); }); + async function getIndexTokens(receiver: string, amount: BigNumber): Promise { + const INDEX_TOKEN_WHALE = "0x9467cfADC9DE245010dF95Ec6a585A506A8ad5FC"; + const indexWhaleSinger = await impersonateAccount(INDEX_TOKEN_WHALE); + await indexToken.connect(indexWhaleSinger).transfer(receiver, amount); + } + addSnapshotBeforeRestoreAfterEach(); context("when auction rebalance extension is added as extension", () => { @@ -163,18 +152,19 @@ if (process.env.INTEGRATIONTEST) { context("when the product settings have been set", () => { let productSettings: any; let identifier: string; - let liveness: BigNumber; + beforeEach(async () => { identifier = utils.formatBytes32String("TestIdentifier"); // TODO: Check how do we ensure that our identifier is supported on UMAs whitelist await identifierWhitelist.addSupportedIdentifier(identifier); - liveness = BigNumber.from(60 * 60); // 7 days + productSettings = { collateral: collateralAssetAddress, liveness, - bondAmount: BigNumber.from(0), + bondAmount: minimumBond, identifier, optimisticOracleV3: optimisticOracleV3.address, }; + await auctionRebalanceExtension .connect(operator) .setProductSettings( @@ -199,6 +189,7 @@ if (process.env.INTEGRATIONTEST) { let subjectPositionMultiplier: BigNumber; let subjectCaller: Signer; let effectiveBond: BigNumber; + beforeEach(async () => { effectiveBond = productSettings.bondAmount.gt(minimumBond) ? productSettings.bondAmount @@ -207,44 +198,108 @@ if (process.env.INTEGRATIONTEST) { subjectQuoteAsset = contractAddresses.tokens.weth; subjectOldComponents = await dsEth.getComponents(); - subjectNewComponents = [contractAddresses.tokens.USDC]; + subjectNewComponents = [contractAddresses.tokens.swETH, contractAddresses.tokens.ETHx]; subjectNewComponentsAuctionParams = [ - { - targetUnit: usdc(100), - priceAdapterName: "ConstantPriceAdapter", - priceAdapterConfigData: await priceAdapter.getEncodedData(ether(0.005)), + { // swETH: https://etherscan.io/address/0xf951E335afb289353dc249e82926178EaC7DEd78#readProxyContract#F6 + targetUnit: ether(0.166), // To do: Check target units + priceAdapterName: "BoundedStepwiseLinearPriceAdapter", + priceAdapterConfigData: await priceAdapter.getEncodedData( + ether(1.043), + ether(0.001), + ONE_HOUR_IN_SECONDS, + true, + ether(1.05), + ether(1.043), + ), + }, + { // ETHx: https://etherscan.io/address/0xcf5ea1b38380f6af39068375516daf40ed70d299#readProxyContract#F5 + targetUnit: ether(0.166), // To do: Check target units + priceAdapterName: "BoundedStepwiseLinearPriceAdapter", + priceAdapterConfigData: await priceAdapter.getEncodedData( + ether(1.014), + ether(0.001), + ONE_HOUR_IN_SECONDS, + true, + ether(1.02), + ether(1.014), + ), }, ]; - const sellAllAuctionParam = { - targetUnit: ether(0), - priceAdapterName: "ConstantPriceAdapter", - priceAdapterConfigData: await priceAdapter.getEncodedData(ether(0.005)), - }; - subjectOldComponentsAuctionParams = subjectOldComponents.map( - () => sellAllAuctionParam, - ); + subjectOldComponentsAuctionParams = [ + { // wstETH: https://etherscan.io/address/0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0#readContract#F10 + targetUnit: ether(0.166), // To do: Check target units + priceAdapterName: "BoundedStepwiseLinearPriceAdapter", + priceAdapterConfigData: await priceAdapter.getEncodedData( + ether(1.155), + ether(0.001), + ONE_HOUR_IN_SECONDS, + true, + ether(1.155), + ether(1.149), + ), + }, + { // rETH: https://etherscan.io/address/0xae78736Cd615f374D3085123A210448E74Fc6393#readContract#F6 + targetUnit: ether(0.166), // To do: Check target units + priceAdapterName: "BoundedStepwiseLinearPriceAdapter", + priceAdapterConfigData: await priceAdapter.getEncodedData( + ether(1.097), + ether(0.001), + ONE_HOUR_IN_SECONDS, + true, + ether(1.097), + ether(1.091), + ), + }, + { // sfrxETH: https://etherscan.io/address/0xac3E018457B222d93114458476f3E3416Abbe38F#readContract#F20 + targetUnit: ether(0.166), // To do: Check target units + priceAdapterName: "BoundedStepwiseLinearPriceAdapter", + priceAdapterConfigData: await priceAdapter.getEncodedData( + ether(1.073), + ether(0.001), + ONE_HOUR_IN_SECONDS, + true, + ether(1.073), + ether(1.067), + ), + }, + { // osETH: Add conversion rate source + targetUnit: ether(0.166), // To do: Check target units + priceAdapterName: "BoundedStepwiseLinearPriceAdapter", + priceAdapterConfigData: await priceAdapter.getEncodedData( + ether(1.005), + ether(0.001), + ONE_HOUR_IN_SECONDS, + true, + ether(1.005), + ether(1.004), + ), + }, + ]; subjectShouldLockSetToken = false; - subjectRebalanceDuration = BigNumber.from(86400); - subjectPositionMultiplier = ether(0.999); + subjectRebalanceDuration = BigNumber.from(60 * 60 * 24 * 3); + subjectPositionMultiplier = await dsEth.positionMultiplier(); subjectCaller = operator; const quantity = utils .parseEther("1000") .add(effectiveBond) .toHexString(); - // set operator balance to effective bond + + // set operator balance to effective bond await ethers.provider.send("hardhat_setBalance", [ await subjectCaller.getAddress(), quantity, ]); - await weth.connect(subjectCaller).deposit({ value: effectiveBond }); - await weth - .connect(subjectCaller) - .approve(auctionRebalanceExtension.address, effectiveBond); - }); + + await getIndexTokens(await subjectCaller.getAddress(), effectiveBond); + await indexToken + .connect(subjectCaller) + .approve(auctionRebalanceExtension.address, effectiveBond); + }); + describe("#startRebalance", () => { async function subject(): Promise { return auctionRebalanceExtension @@ -263,6 +318,7 @@ if (process.env.INTEGRATIONTEST) { context("when the rebalance has been proposed", () => { let proposalId: string; + beforeEach(async () => { const tx = await auctionRebalanceExtension .connect(subjectCaller) @@ -281,6 +337,7 @@ if (process.env.INTEGRATIONTEST) { const assertEvent = receipt.events[receipt.events.length - 1] as any; proposalId = assertEvent.args._assertionId; }); + context("when the liveness period has passed", () => { beforeEach(async () => { await increaseTimeAsync(liveness.add(1)); @@ -302,16 +359,19 @@ if (process.env.INTEGRATIONTEST) { ); await auctionRebalanceExtension.updateIsOpen(true); }); + it("should revert", async () => { await expect(subject()).to.be.revertedWith("Proposal hash does not exist"); }); + context("when identical rebalanced again but liveness has not passed", () => { beforeEach(async () => { // set operator balance to effective bond - await weth.connect(subjectCaller).deposit({ value: effectiveBond }); - await weth + await getIndexTokens(await subjectCaller.getAddress(), effectiveBond); + await indexToken .connect(subjectCaller) .approve(auctionRebalanceExtension.address, effectiveBond); + await auctionRebalanceExtension .connect(subjectCaller) .proposeRebalance( @@ -324,6 +384,7 @@ if (process.env.INTEGRATIONTEST) { subjectPositionMultiplier, ); }); + it("should revert", async () => { await expect(subject()).to.be.revertedWith("Assertion not expired"); }); @@ -389,8 +450,8 @@ if (process.env.INTEGRATIONTEST) { expect(proposal.product).to.eq(dsEth.address); - await weth.connect(subjectCaller).deposit({ value: effectiveBond }); - await weth.connect(subjectCaller).approve(optimisticOracleV3.address, effectiveBond); + await getIndexTokens(await subjectCaller.getAddress(), effectiveBond); + await indexToken.connect(subjectCaller).approve(optimisticOracleV3.address, effectiveBond); await optimisticOracleV3 .connect(subjectCaller) .disputeAssertion(proposalId, owner.address); @@ -400,12 +461,13 @@ if (process.env.INTEGRATIONTEST) { .proposedProduct(utils.formatBytes32String("win")); expect(proposalAfter.product).to.eq(ADDRESS_ZERO); }); + it("should delete the proposal on a disputed callback from currently set oracle", async () => { await auctionRebalanceExtension.connect(operator).setProductSettings( { collateral: collateralAssetAddress, liveness, - bondAmount: BigNumber.from(0), + bondAmount: minimumBond, identifier, optimisticOracleV3: optimisticOracleV3Mock.address, }, @@ -413,6 +475,7 @@ if (process.env.INTEGRATIONTEST) { base58ToHexString("Qmc5gCcjYypU7y28oCALwfSvxCBskLuPKWpK4qpterKC7z"), ), ); + const proposal = await auctionRebalanceExtension .connect(subjectCaller) .proposedProduct(proposalId); @@ -440,7 +503,7 @@ if (process.env.INTEGRATIONTEST) { { collateral: collateralAssetAddress, liveness, - bondAmount: BigNumber.from(0), + bondAmount: minimumBond, identifier, optimisticOracleV3: optimisticOracleV3Mock.address, }, @@ -587,6 +650,7 @@ if (process.env.INTEGRATIONTEST) { .connect(operator) .removeExtension(auctionRebalanceExtension.address); } + it("should remove the extension", async () => { expect(await baseManager.isExtension(auctionRebalanceExtension.address)).to.be.true; await subject(); diff --git a/test/manager/baseManagerV2.spec.ts b/test/manager/baseManagerV2.spec.ts index e2b00552..977e7316 100644 --- a/test/manager/baseManagerV2.spec.ts +++ b/test/manager/baseManagerV2.spec.ts @@ -371,8 +371,8 @@ describe("BaseManagerV2", () => { }); describe("when the extension is authorized for a protected module", () => { - beforeEach(() => { - baseManager.connect(operator.wallet).protectModule(subjectModule, [subjectExtension]); + beforeEach(async () => { + await baseManager.connect(operator.wallet).protectModule(subjectModule, [subjectExtension]); }); it("should revert", async() => { diff --git a/utils/common/conversionUtils.ts b/utils/common/conversionUtils.ts index 1dcd2430..056dc297 100644 --- a/utils/common/conversionUtils.ts +++ b/utils/common/conversionUtils.ts @@ -1,3 +1,22 @@ import { BigNumber } from "ethers/lib/ethers"; +import base58 from "bs58"; -export const bigNumberToData = (number: BigNumber) => number.toHexString().replace("0x", "").padStart(64, "0"); \ No newline at end of file +export const bigNumberToData = (number: BigNumber) => number.toHexString().replace("0x", "").padStart(64, "0"); + +export const bufferToHex = (buffer: Uint8Array) => { + let hexStr = ""; + + for (let i = 0; i < buffer.length; i++) { + const hex = (buffer[i] & 0xff).toString(16); + hexStr += hex.length === 1 ? "0" + hex : hex; + } + + return hexStr; +}; + +// Base58 decoding function (make sure you have a proper Base58 decoding function) +export const base58ToHexString = (base58String: string) => { + const bytes = base58.decode(base58String); // Decode base58 to a buffer + const hexString = bufferToHex(bytes.slice(2)); // Convert buffer to hex, excluding the first 2 bytes + return "0x" + hexString; +}; diff --git a/utils/common/index.ts b/utils/common/index.ts index eeac5056..d50dd8dd 100644 --- a/utils/common/index.ts +++ b/utils/common/index.ts @@ -21,5 +21,7 @@ export { convertLibraryNameToLinkId } from "./libraryUtils"; export { - bigNumberToData + bigNumberToData, + bufferToHex, + base58ToHexString, } from "./conversionUtils"; diff --git a/utils/contracts/setV2.ts b/utils/contracts/setV2.ts index 295237ad..91c39c8d 100644 --- a/utils/contracts/setV2.ts +++ b/utils/contracts/setV2.ts @@ -4,6 +4,7 @@ export { AaveV2 } from "../../typechain/AaveV2"; export { AirdropModule } from "../../typechain/AirdropModule"; export { AuctionRebalanceModuleV1 } from "../../typechain/AuctionRebalanceModuleV1"; export { BasicIssuanceModule } from "../../typechain/BasicIssuanceModule"; +export { BoundedStepwiseLinearPriceAdapter } from "../../typechain/BoundedStepwiseLinearPriceAdapter"; export { Compound } from "../../typechain/Compound"; export { Controller } from "../../typechain/Controller"; export { ContractCallerMock } from "../../typechain/ContractCallerMock";