Skip to content

Instantly share code, notes, and snippets.

@wesleyfsmith
Last active June 28, 2023 18:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wesleyfsmith/f43c0ee699fdeb5aa38a0b54438d478b to your computer and use it in GitHub Desktop.
Save wesleyfsmith/f43c0ee699fdeb5aa38a0b54438d478b to your computer and use it in GitHub Desktop.
SureFX v1 contract
//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