Last active
June 28, 2023 18:23
-
-
Save wesleyfsmith/f43c0ee699fdeb5aa38a0b54438d478b to your computer and use it in GitHub Desktop.
SureFX v1 contract
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//SPDX-License-Identifier: | |
pragma solidity 0.8.10; | |
import "hardhat/console.sol"; | |
import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; | |
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; | |
import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; | |
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | |
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | |
import {FixedPointMathLib} from "solmate/src/utils/FixedPointMathLib.sol"; | |
import {IHedgeManager} from "./interfaces/IHedgeManager.sol"; | |
contract HedgeManager is | |
Initializable, | |
AccessControlUpgradeable, | |
PausableUpgradeable, | |
IHedgeManager | |
{ | |
using FixedPointMathLib for uint256; | |
using SafeERC20 for IERC20; | |
/*////////////////////////////////////////////////////////////// | |
CONSTANTS | |
//////////////////////////////////////////////////////////////*/ | |
uint256 public constant SCSCALAR = 10 ** 6; | |
bytes32 public constant LP = keccak256("LP"); | |
bytes32 public constant VARSETTER = keccak256("VARSETTER"); | |
bytes32 public constant LIQUIDATOR = keccak256("LIQUIDATOR"); | |
bytes32 public constant SETEXCHANGERATE = keccak256("SETEXCHANGERATE"); | |
/*////////////////////////////////////////////////////////////// | |
VARS | |
//////////////////////////////////////////////////////////////*/ | |
IERC20 public usdcContract; | |
IERC20 public copcContract; | |
// exchange rate, expressed as a fix point number with a 10**6 scalar | |
uint256 public usdToCopRate; | |
// the target of withdrawn liquidity | |
address public withdrawTarget; | |
// grace period after end of hedge | |
uint256 public lockupTime; | |
// max time duration of any hedge | |
uint256 public maxHedgeDuration; | |
// last time usdToCopRate var was updated | |
uint256 public lastUpdateExchangeRate; | |
// max amount of time that can pass before the current value of usdToCopRate becomes invalid | |
uint256 public maxExchangeRateLife; | |
// ratio of the hedge amount that must be locked as collat, expressed as decimal*10^6 | |
uint256 public collateralizationRatio; | |
// ratio of the hedge amount that must be paid as a fee,expressed as decimal*10^6 | |
uint256 public platformFee; | |
// ratio of collateral that describes the minimum to be liquidated,expressed as decimal*10^6 | |
uint256 public liquidationRatio; | |
// hedge id counter | |
uint256 public latestHedgeId; | |
// balances caught up in active positions | |
uint256 public copcBalanceCaughtUpInHedge; | |
uint256 public usdcBalanceCaughtUpInHedge; | |
// fee counter for each token | |
uint256 public feeReservesCopc; | |
uint256 public feeReservesUsdc; | |
// hedge info storage | |
mapping(uint256 => Hedge) private hedges; | |
mapping(address => uint256[]) private ownerToHedgeIds; | |
/*////////////////////////////////////////////////////////////// | |
CONSTRUCTOR | |
//////////////////////////////////////////////////////////////*/ | |
function initialize( | |
address _usdcAddress, | |
address _copcAddress | |
) external initializer { | |
__Pausable_init(); | |
__AccessControl_init(); | |
withdrawTarget = msg.sender; | |
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender); | |
_grantRole(LP, msg.sender); | |
_grantRole(VARSETTER, msg.sender); | |
_grantRole(LIQUIDATOR, msg.sender); | |
_grantRole(SETEXCHANGERATE, msg.sender); | |
usdcContract = IERC20(_usdcAddress); | |
copcContract = IERC20(_copcAddress); | |
lockupTime = 24 hours; | |
maxHedgeDuration = 90 days; | |
collateralizationRatio = .1 * 10 ** 6; | |
platformFee = .03 * 10 ** 6; | |
liquidationRatio = .75 * 10 ** 6; | |
maxExchangeRateLife = 24 hours; | |
lastUpdateExchangeRate = block.timestamp; | |
} | |
/*/////////////////////////////////////////////////////////////// | |
MAIN LOGIC | |
//////////////////////////////////////////////////////////////*/ | |
function liquidateHedge( | |
uint256 _hedgeId | |
) external whenNotPaused onlyRole(LIQUIDATOR) { | |
// hedge id starts at 1 | |
require(0 != _hedgeId && _hedgeId <= latestHedgeId, "HedgeIdNotExist"); | |
Hedge storage hedge = hedges[_hedgeId]; | |
require(isHedgeLiquidatable(_hedgeId), "PositionNotLiquidatable"); | |
hedges[_hedgeId].liquidated = true; | |
hedges[_hedgeId].closeDate = block.timestamp; | |
if (hedges[_hedgeId].direction == ConversionDirection.USD_TO_COP) { | |
// funds will now be withdrawbale by the LP role | |
usdcBalanceCaughtUpInHedge = | |
usdcBalanceCaughtUpInHedge - | |
(hedge.amount + hedge.collateral); | |
} else { | |
// funds will now be withdrawbale by the LP role | |
copcBalanceCaughtUpInHedge = | |
copcBalanceCaughtUpInHedge - | |
(hedge.amount + hedge.collateral); | |
} | |
emit HedgeLiquidated(_hedgeId, msg.sender); | |
} | |
function isHedgeLiquidatable( | |
uint256 _hedgeId | |
) public view returns (bool liquidatable) { | |
Hedge memory hedge = hedges[_hedgeId]; | |
//hedge cannot be closed or liquidated | |
require( | |
hedge.closed == false && hedge.liquidated == false, | |
"closedOrLiquidated" | |
); | |
uint256 left; | |
uint256 right; | |
if (hedge.direction == ConversionDirection.USD_TO_COP) { | |
// original amount of cop that was transferred to the user when opening the position | |
uint256 originalAmountOUT = hedge.amount.mulDivDown( | |
hedge.lockedInRate, | |
SCSCALAR | |
); | |
// cop (left) < usd + (usdCollat * .75) (right) | all in usd terms | |
left = originalAmountOUT.mulDivDown(SCSCALAR, getExchangeRate()); | |
// already in usd terms | |
right = (hedge.amount + | |
hedge.collateral.mulDivDown(liquidationRatio, SCSCALAR)); | |
} else { | |
// usd (left) < cop + (copCollat * .75) (right) | all in usd terms | |
left = hedge.amount.mulDivDown(SCSCALAR, hedge.lockedInRate); | |
// hedge.amount and hedge.collateral is in COP | |
// (hedge.amount + hedge.collateral * .75)/rate to make it in terms of usd | |
right = (hedge.amount + | |
hedge.collateral.mulDivDown(liquidationRatio, SCSCALAR)) | |
.mulDivDown(SCSCALAR, getExchangeRate()); | |
} | |
// hedge can be liquidated if position is undercollateralized or overdue | |
liquidatable = | |
(left >= right) || | |
(block.timestamp >= hedge.startDate + hedge.duration + lockupTime); | |
} | |
function addCollateral( | |
uint256 _hedgeId, | |
uint256 _amount | |
) external whenNotPaused { | |
Hedge storage hedge = hedges[_hedgeId]; | |
// only hedge or admin can close position | |
// this will also implicitly check if the hedgeid is valid | |
require( | |
msg.sender == hedge.owner || | |
hasRole(DEFAULT_ADMIN_ROLE, msg.sender), | |
"NotAllowed" | |
); | |
// hedge cannot be closed or liquidated | |
require( | |
hedge.closed == false && hedge.liquidated == false, | |
"HedgeClosed" | |
); | |
hedge.collateral = hedge.collateral + _amount; | |
if (hedge.direction == ConversionDirection.USD_TO_COP) { | |
usdcBalanceCaughtUpInHedge = usdcBalanceCaughtUpInHedge + _amount; | |
usdcContract.safeTransferFrom(msg.sender, address(this), _amount); | |
} else { | |
copcBalanceCaughtUpInHedge = copcBalanceCaughtUpInHedge + _amount; | |
copcContract.safeTransferFrom(msg.sender, address(this), _amount); | |
} | |
emit CollateralAdded(_hedgeId, _amount, msg.sender); | |
} | |
function closeHedge(uint256 _hedgeId) external whenNotPaused { | |
// load everything to memory to avoid many sloads. | |
Hedge memory hedge = hedges[_hedgeId]; | |
// only hedge or admin can close position | |
// this will also implicitly check if the hedgeid is valid | |
require( | |
msg.sender == hedge.owner || | |
hasRole(DEFAULT_ADMIN_ROLE, msg.sender), | |
"NotAllowed" | |
); | |
// hedge cannot be closed or liquidated | |
require( | |
hedge.closed == false && hedge.liquidated == false, | |
"HedgeClosed" | |
); | |
hedges[_hedgeId].closed = true; | |
hedges[_hedgeId].closeDate = block.timestamp; | |
if (hedge.direction == ConversionDirection.USD_TO_COP) { | |
usdcBalanceCaughtUpInHedge = | |
usdcBalanceCaughtUpInHedge - | |
(hedge.amount + hedge.collateral); | |
copcContract.safeTransferFrom( | |
msg.sender, | |
address(this), | |
// (hedge.amount * hedge.lockedInRate)/10^6 | |
hedge.amount.mulDivDown(hedge.lockedInRate, SCSCALAR) | |
); | |
usdcContract.safeTransfer( | |
msg.sender, | |
hedge.amount + hedge.collateral | |
); | |
} else { | |
copcBalanceCaughtUpInHedge = | |
copcBalanceCaughtUpInHedge - | |
(hedge.amount + hedge.collateral); | |
usdcContract.safeTransferFrom( | |
msg.sender, | |
address(this), | |
// (hedge.amount * 10^6)/hedge.lockedInRate | |
hedge.amount.mulDivDown(SCSCALAR, hedge.lockedInRate) | |
); | |
copcContract.safeTransfer( | |
msg.sender, | |
hedge.amount + hedge.collateral | |
); | |
} | |
emit HedgeClosed(_hedgeId, msg.sender); | |
} | |
function openHedge( | |
ConversionDirection _direction, | |
uint256 _duration, | |
uint256 _amount | |
) external whenNotPaused returns (uint256) { | |
require(_duration <= maxHedgeDuration, "OverMaxDuration"); | |
uint256 collateralRequirement = getCollateralRequirement(_amount); | |
uint256 feeRequirement = getFee(_amount); | |
uint256 totalAmount = _amount + collateralRequirement + feeRequirement; | |
uint256 convertedAmount = getAmountForCurrentExchangeRate( | |
_amount, | |
_direction | |
); | |
if (_direction == ConversionDirection.USD_TO_COP) { | |
require( | |
convertedAmount <= | |
copcContract.balanceOf(address(this)) - | |
copcBalanceCaughtUpInHedge, | |
"noLiquidity" | |
); | |
feeReservesUsdc = feeReservesUsdc + feeRequirement; | |
usdcBalanceCaughtUpInHedge = | |
usdcBalanceCaughtUpInHedge + | |
(_amount + collateralRequirement); | |
usdcContract.safeTransferFrom( | |
msg.sender, | |
address(this), | |
totalAmount | |
); | |
copcContract.safeTransfer(msg.sender, convertedAmount); | |
} else { | |
require( | |
convertedAmount <= | |
usdcContract.balanceOf(address(this)) - | |
usdcBalanceCaughtUpInHedge, | |
"noLiquidity" | |
); | |
feeReservesCopc = feeReservesCopc + feeRequirement; | |
copcBalanceCaughtUpInHedge = | |
copcBalanceCaughtUpInHedge + | |
(_amount + collateralRequirement); | |
copcContract.safeTransferFrom( | |
msg.sender, | |
address(this), | |
totalAmount | |
); | |
usdcContract.safeTransfer(msg.sender, convertedAmount); | |
} | |
// hedgeId starts at one and increments each time | |
latestHedgeId = latestHedgeId + 1; | |
Hedge memory hedge = Hedge({ | |
direction: _direction, | |
id: latestHedgeId, | |
amount: _amount, | |
collateral: collateralRequirement, | |
fee: feeRequirement, | |
startDate: block.timestamp, | |
duration: _duration, | |
lockedInRate: getExchangeRate(), | |
closeDate: 0, | |
owner: msg.sender, | |
closed: false, | |
liquidated: false | |
}); | |
hedges[latestHedgeId] = hedge; | |
ownerToHedgeIds[msg.sender].push(latestHedgeId); | |
emit NewHedge(latestHedgeId, msg.sender); | |
} | |
/*/////////////////////////////////////////////////////////////// | |
DEPOSIT/WITHDRAW LP | |
//////////////////////////////////////////////////////////////*/ | |
function addCopcLiquidity(uint256 _amount) external onlyRole(LP) { | |
copcContract.safeTransferFrom(msg.sender, address(this), _amount); | |
emit LiquidityDeposited(address(copcContract), _amount, msg.sender); | |
} | |
function addUsdcLiquidity(uint256 _amount) external onlyRole(LP) { | |
usdcContract.safeTransferFrom(msg.sender, address(this), _amount); | |
emit LiquidityDeposited(address(usdcContract), _amount, msg.sender); | |
} | |
function withdrawCopcLiquiditySAFE(uint256 _amount) external onlyRole(LP) { | |
uint256 balance = copcContract.balanceOf(address(this)); | |
require( | |
_amount <= (balance - copcBalanceCaughtUpInHedge), | |
"NotEnoughtLiquidty" | |
); | |
copcContract.safeTransfer(withdrawTarget, _amount); | |
emit LiquidityWithdrawn(address(copcContract), _amount, msg.sender); | |
} | |
function withdrawUsdLiquiditySAFE(uint256 _amount) external onlyRole(LP) { | |
uint256 balance = usdcContract.balanceOf(address(this)); | |
require( | |
_amount <= (balance - usdcBalanceCaughtUpInHedge), | |
"NotEnoughtLiquidty" | |
); | |
usdcContract.safeTransfer(withdrawTarget, _amount); | |
emit LiquidityWithdrawn(address(usdcContract), _amount, msg.sender); | |
} | |
function withdrawFeeReservesCopc(uint256 _amount) external onlyRole(LP) { | |
require(_amount <= feeReservesCopc, "NotEnoughtFees"); | |
feeReservesCopc = feeReservesCopc - _amount; | |
copcContract.safeTransfer(withdrawTarget, _amount); | |
emit WithdrawFees(address(copcContract), _amount, msg.sender); | |
} | |
function withdrawFeeReservesUsdc(uint256 _amount) external onlyRole(LP) { | |
require(_amount <= feeReservesUsdc, "NotEnoughtFees"); | |
feeReservesUsdc = feeReservesUsdc - _amount; | |
usdcContract.safeTransfer(withdrawTarget, _amount); | |
emit WithdrawFees(address(usdcContract), _amount, msg.sender); | |
} | |
function withdrawCopcPANIC() external onlyRole(DEFAULT_ADMIN_ROLE) { | |
copcContract.safeTransfer( | |
msg.sender, | |
copcContract.balanceOf(address(this)) | |
); | |
} | |
function withdrawUsdcPANIC() external onlyRole(DEFAULT_ADMIN_ROLE) { | |
usdcContract.safeTransfer( | |
msg.sender, | |
usdcContract.balanceOf(address(this)) | |
); | |
} | |
/*/////////////////////////////////////////////////////////////// | |
GETTER FUNCTIONS | |
//////////////////////////////////////////////////////////////*/ | |
function getHedges( | |
address _owner | |
) external view returns (uint256[] memory) { | |
return ownerToHedgeIds[_owner]; | |
} | |
function getHedge(uint256 _hedgeId) external view returns (Hedge memory) { | |
return hedges[_hedgeId]; | |
} | |
function getExchangeRate() public view returns (uint256 currentRate) { | |
require( | |
block.timestamp - lastUpdateExchangeRate < maxExchangeRateLife, | |
"ExchangeRateOld" | |
); | |
currentRate = usdToCopRate; | |
require(currentRate != 0, "ExchangeRateZero"); | |
} | |
function getCollateralRequirement( | |
uint256 _amount | |
) public view returns (uint256 requiredCollateral) { | |
// (amount*collateralizationRatio)/10^6 | |
requiredCollateral = _amount.mulDivDown( | |
collateralizationRatio, | |
SCSCALAR | |
); | |
} | |
function getFee(uint256 _amount) public view returns (uint256 requiredFee) { | |
// (_amount*platformFee)/10^6 | |
requiredFee = _amount.mulDivDown(platformFee, SCSCALAR); | |
} | |
function getAmountForExchangeRate( | |
uint256 _amount, | |
ConversionDirection _direction, | |
uint256 _usdToCopRate | |
) public pure returns (uint256 amount) { | |
// minimum 1 usd | |
if (_direction == ConversionDirection.USD_TO_COP) { | |
require(_amount > SCSCALAR, "MinOneUSD"); | |
// (_amount*_usdToCopRate)/10^6 | |
amount = _amount.mulDivDown(_usdToCopRate, SCSCALAR); | |
} else if (_direction == ConversionDirection.COP_TO_USD) { | |
require(_amount > _usdToCopRate, "MinOneUSD"); | |
// (_amount*10^6)/_usdToCopRate | |
amount = _amount.mulDivDown(SCSCALAR, _usdToCopRate); | |
} | |
} | |
function getAmountForCurrentExchangeRate( | |
uint256 _amount, | |
ConversionDirection _direction | |
) public view returns (uint256 amount) { | |
amount = getAmountForExchangeRate( | |
_amount, | |
_direction, | |
getExchangeRate() | |
); | |
} | |
/*/////////////////////////////////////////////////////////////// | |
SETTER FUNCTIONS | |
//////////////////////////////////////////////////////////////*/ | |
function setExchangeRate( | |
uint256 _newRate | |
) external onlyRole(SETEXCHANGERATE) { | |
// set the curret exchange rate of usd -> pesos | |
uint256 oldRate = usdToCopRate; | |
lastUpdateExchangeRate = block.timestamp; | |
// use 10^6 scalar | |
usdToCopRate = _newRate; | |
emit ExchangeRateChanged( | |
oldRate, | |
_newRate, | |
lastUpdateExchangeRate, | |
msg.sender | |
); | |
} | |
function setWithdrawTarget( | |
address _newTarget | |
) external onlyRole(DEFAULT_ADMIN_ROLE) { | |
address oldTarget = withdrawTarget; | |
withdrawTarget = _newTarget; | |
emit ChangeWithdrawTarget(oldTarget, _newTarget); | |
} | |
function setVarToken( | |
bytes32 what, | |
IERC20 value | |
) external onlyRole(VARSETTER) { | |
if (what == "copcContract") copcContract = value; | |
else if (what == "usdcContract") usdcContract = value; | |
else revert InvalidParameter(what); | |
emit VarSet(what, abi.encode(value), msg.sender); | |
} | |
function setVarInt( | |
bytes32 what, | |
uint256 value | |
) external onlyRole(VARSETTER) { | |
require(value != 0, "ValueZero"); | |
if (what == "platformFee") platformFee = value; | |
else if (what == "lockupTime") lockupTime = value; | |
else if (what == "maxHedgeDuration") maxHedgeDuration = value; | |
else if (what == "maxExchangeRateLife") maxExchangeRateLife = value; | |
else if (what == "collateralizationRatio") | |
collateralizationRatio = value; | |
else if (what == "liquidationRatio") liquidationRatio = value; | |
else revert InvalidParameter(what); | |
emit VarSet(what, abi.encode(value), msg.sender); | |
} | |
/*/////////////////////////////////////////////////////////////// | |
FREEZ LOGIC | |
//////////////////////////////////////////////////////////////*/ | |
function pause() external onlyRole(DEFAULT_ADMIN_ROLE) { | |
_pause(); | |
} | |
function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { | |
_unpause(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment