Skip to content

Instantly share code, notes, and snippets.

@voith
Last active July 2, 2023 15:34
Show Gist options
  • Save voith/96a2a0b495d7987afa88b31d7b9f8062 to your computer and use it in GitHub Desktop.
Save voith/96a2a0b495d7987afa88b31d7b9f8062 to your computer and use it in GitHub Desktop.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract ChainlinkTCAPAggregatorV3 {
struct RoundData {
uint80 roundId;
uint80 answeredInRound;
int256 answer;
uint256 startedAt;
uint256 updatedAt;
}
mapping(uint80 => RoundData) roundDataHistory;
error RoundIdMissing();
RoundData _latestRoundData =
RoundData({
roundId: uint80(18446744073709551734),
answer: int256(122149510910889330000),
startedAt: uint256(1679508968),
updatedAt: uint256(1679508968),
answeredInRound: uint80(18446744073709551734)
});
constructor() {
roundDataHistory[_latestRoundData.roundId] = _latestRoundData;
}
function decimals() external view returns (uint8) {
return uint8(8);
}
function getRoundData(
uint80 _roundId
)
external
view
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
)
{
RoundData memory roundData = roundDataHistory[_roundId];
if (roundData.roundId != _roundId) revert RoundIdMissing();
roundId = _roundId;
answer = roundData.answer;
startedAt = roundData.startedAt;
updatedAt = roundData.updatedAt;
answeredInRound = roundData.answeredInRound;
}
function latestRoundData()
external
view
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
)
{
roundId = _latestRoundData.roundId;
answer = _latestRoundData.answer;
startedAt = _latestRoundData.startedAt;
updatedAt = _latestRoundData.updatedAt;
answeredInRound = _latestRoundData.answeredInRound;
}
function next() external {
_next(101);
}
function nextLow() external {
_next(90);
}
function nextSame() external {
_next(100);
}
function _next(int256 multiple) internal {
_latestRoundData.roundId = _latestRoundData.roundId + 1;
_latestRoundData.answer = (_latestRoundData.answer * multiple) / 100;
_latestRoundData.startedAt = _latestRoundData.startedAt + 1000;
_latestRoundData.updatedAt = _latestRoundData.updatedAt + 1000;
_latestRoundData.answeredInRound = _latestRoundData.answeredInRound + 1;
roundDataHistory[_latestRoundData.roundId] = _latestRoundData;
}
}
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;
import "@equilibria/perennial/contracts/interfaces/IContractPayoffProvider.sol";
contract TcapPayoffProvider is IContractPayoffProvider {
function payoff(Fixed18 price) external pure override returns (Fixed18) {
return price.div(Fixed18Lib.from(10000000000));
}
}
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol";
import "@equilibria/emptyset-batcher/interfaces/IBatcher.sol";
import "@equilibria/root/token/types/Token18.sol";
import "@equilibria/root/token/types/Token6.sol";
import "./TestnetReserve.sol";
contract TestnetBatcher is IBatcher {
IEmptySetReserve public RESERVE;
Token6 public USDC;
Token18 public DSU;
constructor(IEmptySetReserve reserve_, Token6 usdc_, Token18 dsu_) {
RESERVE = reserve_;
USDC = usdc_;
DSU = dsu_;
USDC.approve(address(RESERVE));
DSU.approve(address(RESERVE));
}
function totalBalance() external pure returns (UFixed18) {
return UFixed18Lib.MAX;
}
// Passthrough to Reserve
function wrap(UFixed18 amount, address to) external {
USDC.pull(msg.sender, amount, true);
RESERVE.mint(amount);
DSU.push(to, amount);
emit Wrap(to, amount);
}
// Passthrough to Reserve
function unwrap(UFixed18 amount, address to) external {
DSU.pull(msg.sender, amount);
RESERVE.redeem(amount);
USDC.push(to, amount);
emit Unwrap(to, amount);
}
// No-op
function rebalance() external pure {
return;
}
}
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
contract TestnetDSU is ERC20, ERC20Burnable {
uint256 private constant LIMIT = 1_000_000e18;
address public minter;
error TestnetDSUNotMinterError();
error TestnetDSUOverLimitError();
event TestnetDSUMinterUpdated(address indexed newMinter);
constructor(address _minter) ERC20("Digital Standard Unit", "DSU") {
minter = _minter;
}
function mint(address account, uint256 amount) external onlyMinter {
if (amount > LIMIT) revert TestnetDSUOverLimitError();
_mint(account, amount);
}
function updateMinter(address newMinter) external onlyMinter {
minter = newMinter;
emit TestnetDSUMinterUpdated(newMinter);
}
modifier onlyMinter() {
if (msg.sender != minter) revert TestnetDSUNotMinterError();
_;
}
}
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@equilibria/emptyset-batcher/interfaces/IBatcher.sol";
import "@equilibria/root/token/types/Token18.sol";
import "@equilibria/root/token/types/Token6.sol";
contract TestnetReserve is IEmptySetReserve {
Token18 public immutable DSU; // solhint-disable-line var-name-mixedcase
Token6 public immutable USDC; // solhint-disable-line var-name-mixedcase
constructor(Token18 dsu_, Token6 usdc_) {
DSU = dsu_;
USDC = usdc_;
}
function mint(UFixed18 amount) external {
USDC.pull(msg.sender, amount, true);
ERC20PresetMinterPauser(Token18.unwrap(DSU)).mint(
msg.sender,
UFixed18.unwrap(amount)
);
uint256 pulledAmount = Math.ceilDiv(UFixed18.unwrap(amount), 1e12);
emit Mint(msg.sender, UFixed18.unwrap(amount), pulledAmount);
}
function redeem(UFixed18 amount) external {
DSU.pull(msg.sender, amount);
ERC20Burnable(Token18.unwrap(DSU)).burn(UFixed18.unwrap(amount));
USDC.push(msg.sender, amount, true);
uint256 pushedAmount = UFixed18.unwrap(amount) / 1e12;
emit Redeem(msg.sender, UFixed18.unwrap(amount), pushedAmount);
}
function debt(address) external pure returns (UFixed18) {
return UFixed18Lib.ZERO;
}
function repay(address, UFixed18) external pure {
return;
}
}
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
contract TestnetUSDC is ERC20, ERC20Burnable {
// solhint-disable-next-line no-empty-blocks
constructor() ERC20("USD Coin", "USDC") {}
function decimals() public pure override returns (uint8) {
return 6;
}
function mint(address account, uint256 amount) external {
_mint(account, amount);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "forge-std/console.sol";
import "@equilibria/perennial/contracts/collateral/Collateral.sol";
import "@equilibria/perennial/contracts/product/Product.sol";
import "@equilibria/perennial/contracts/incentivizer/Incentivizer.sol";
import "@equilibria/perennial/contracts/controller/Controller.sol";
import "@equilibria/perennial/contracts/forwarder/Forwarder.sol";
import "@equilibria/perennial/contracts/interfaces/types/PayoffDefinition.sol";
import "@equilibria/perennial/contracts/lens/PerennialLens.sol";
import "@equilibria/perennial/contracts/multiinvoker/MultiInvoker.sol";
import "@equilibria/perennial-oracle/contracts/ChainlinkFeedOracle.sol";
import "@equilibria/perennial-oracle/contracts/types/ChainlinkAggregator.sol";
import "@equilibria/perennial-vaults/contracts/BalancedVault.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/governance/TimelockController.sol";
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "./TestnetUSDC.sol";
import "./TestnetDSU.sol";
import "./TestnetReserve.sol";
import "./TestnetBatcher.sol";
import "./ChainlinkTCAPAggregatorV3.sol";
import "./TcapPayoffProvider.sol";
contract IntegrationTest is Test {
TestnetUSDC USDC;
TestnetDSU DSU;
TestnetReserve reserve;
TestnetBatcher batcher;
Collateral collateral;
Product product;
Incentivizer incentivizer;
Controller controller;
Collateral collateralImpl;
Product productImpl;
Incentivizer incentivizerImpl;
Controller controllerImpl;
TimelockController timelock;
ProxyAdmin proxyAdmin;
UpgradeableBeacon productBeacon;
TransparentUpgradeableProxy incentivizerProxy;
TransparentUpgradeableProxy collateralProxy;
TransparentUpgradeableProxy controllerProxy;
PerennialLens lens;
MultiInvoker multiInvokerImpl;
TransparentUpgradeableProxy multiInvokerProxy;
MultiInvoker multiInvoker;
Forwarder forwarder;
BalancedVault vaultImpl;
TransparentUpgradeableProxy vaultProxy;
BalancedVault vault;
IProduct long;
IProduct short;
ChainlinkTCAPAggregatorV3 tcapOracle;
Fixed18 makerFeeRate =
Fixed18Lib.from(int256(15)).div(Fixed18Lib.from(int256(1000)));
Fixed18 takerFeeRate =
Fixed18Lib.from(int256(15)).div(Fixed18Lib.from(int256(1000)));
UFixed18 initialCollateral = UFixed18Lib.from(20000);
Fixed18 makerPosition =
Fixed18Lib.from(int256(1)).div(Fixed18Lib.from(int256(1000))); // 0.001
Fixed18 takerPosition =
Fixed18Lib.from(int256(1)).div(Fixed18Lib.from(int256(1000))); // 0.001
// cryptex controlled contracts
uint256 coordinatorID;
address perennialOwner = address(0x51);
address cryptexOwner = address(0x52);
address userA = address(0x53);
address userB = address(0x54);
address userC = address(0x55);
address cryptexTreasury = address(0x56);
address perennialTreasury = address(0x57);
event AccountSettle(
IProduct indexed product,
address indexed account,
Fixed18 amount,
UFixed18 newShortfall
);
function setUp() external {
vm.startPrank(perennialOwner);
USDC = new TestnetUSDC();
DSU = new TestnetDSU(perennialOwner);
reserve = new TestnetReserve(
Token18.wrap(address(DSU)),
Token6.wrap(address(USDC))
);
batcher = new TestnetBatcher(
reserve,
Token6.wrap(address(USDC)),
Token18.wrap(address(DSU))
);
collateralImpl = new Collateral(Token18.wrap(address(DSU)));
productImpl = new Product();
incentivizerImpl = new Incentivizer();
controllerImpl = new Controller();
address[] memory proposers = new address[](1);
address[] memory executors = new address[](1);
proposers[0] = perennialOwner;
executors[0] = address(0x0);
timelock = new TimelockController(60, proposers, executors);
proxyAdmin = new ProxyAdmin();
productBeacon = new UpgradeableBeacon(address(productImpl));
incentivizerProxy = new TransparentUpgradeableProxy(
address(incentivizerImpl),
address(proxyAdmin),
bytes("")
);
incentivizer = Incentivizer(address(incentivizerProxy));
collateralProxy = new TransparentUpgradeableProxy(
address(collateralImpl),
address(proxyAdmin),
bytes("")
);
collateral = Collateral(address(collateralProxy));
controllerProxy = new TransparentUpgradeableProxy(
address(controllerImpl),
address(proxyAdmin),
bytes("")
);
controller = Controller(address(controllerProxy));
incentivizer.initialize(controller);
collateral.initialize(controller);
controller.initialize(collateral, incentivizer, productBeacon);
controller.updateCoordinatorPendingOwner(0, perennialOwner);
controller.updateCoordinatorTreasury(0, perennialTreasury);
controller.updateProtocolFee(UFixed18.wrap(0));
lens = new PerennialLens(controller);
forwarder = new Forwarder(
Token6.wrap(address(USDC)),
Token18.wrap(address(DSU)),
batcher,
collateral
);
multiInvokerImpl = new MultiInvoker(
Token6.wrap(address(USDC)),
batcher,
reserve,
controller
);
multiInvokerProxy = new TransparentUpgradeableProxy(
address(multiInvokerImpl),
address(proxyAdmin),
bytes("")
);
multiInvoker = MultiInvoker(address(multiInvokerProxy));
multiInvoker.initialize();
vm.stopPrank();
cryptexSetup();
}
function parseEther(uint256 value) public returns (uint256) {
return value * 10 ** 18;
}
function cryptexSetup() public {
vm.startPrank(cryptexOwner);
coordinatorID = controller.createCoordinator();
tcapOracle = new ChainlinkTCAPAggregatorV3();
ChainlinkFeedOracle oracle = new ChainlinkFeedOracle(
ChainlinkAggregator.wrap(address(tcapOracle))
);
TcapPayoffProvider payoffProvider = new TcapPayoffProvider();
IProduct.ProductInfo memory productInfo = IProduct.ProductInfo({
name: "Total Market Cap",
symbol: "TCAP",
payoffDefinition: PayoffDefinition({
payoffType: PayoffDefinitionLib.PayoffType.CONTRACT,
payoffDirection: PayoffDefinitionLib.PayoffDirection.LONG,
data: bytes30(bytes20(address(payoffProvider))) >> 80
}),
oracle: oracle,
maintenance: UFixed18Lib.from(10).div(UFixed18Lib.from(100)),
fundingFee: UFixed18Lib.from(0).div(UFixed18Lib.from(100)),
makerFee: UFixed18Lib.from(15).div(UFixed18Lib.from(1000)),
takerFee: UFixed18Lib.from(15).div(UFixed18Lib.from(1000)),
positionFee: UFixed18Lib.from(100).div(UFixed18Lib.from(100)),
makerLimit: UFixed18.wrap(parseEther(4000)),
utilizationCurve: JumpRateUtilizationCurve({
minRate: Fixed18Lib.from(int256(0)).pack(),
maxRate: Fixed18Lib
.from(int256(0))
.div(Fixed18Lib.from(int256(100)))
.pack(),
targetRate: Fixed18Lib
.from(int256(6))
.div(Fixed18Lib.from(int256(100)))
.pack(),
targetUtilization: UFixed18Lib
.from(0)
.div(UFixed18Lib.from(100))
.pack()
})
});
long = controller.createProduct(coordinatorID, productInfo);
productInfo.payoffDefinition.payoffDirection = PayoffDefinitionLib
.PayoffDirection
.SHORT;
short = controller.createProduct(coordinatorID, productInfo);
controller.updateCoordinatorTreasury(coordinatorID, cryptexTreasury);
vaultImpl = new BalancedVault(
Token18.wrap(address(DSU)),
controller,
long,
short,
UFixed18.wrap(parseEther(25) / 10),
UFixed18.wrap(parseEther(3000000))
);
vaultProxy = new TransparentUpgradeableProxy(address(vaultImpl), address(proxyAdmin), bytes(''));
vault = BalancedVault(address(vaultProxy));
vault.initialize('Cryptex Vault Alpha', 'CVA');
vm.stopPrank();
vm.deal(userA, 30000 ether);
vm.deal(userB, 30000 ether);
vm.deal(userC, 30000 ether);
deal({token: address(DSU), to: userA, give: 1000000 ether});
deal({token: address(DSU), to: userB, give: 1000000 ether});
deal({token: address(DSU), to: userC, give: 1000000 ether});
tcapOracle.next();
}
function UFixed18ToUint(UFixed18 value) internal returns(uint256){
return UFixed18.unwrap(value);
}
function depositToVault(UFixed18 assets, address account) internal {
vm.startPrank(account);
DSU.approve(address(vault), UFixed18.unwrap(assets));
vault.deposit(assets, account);
vm.stopPrank();
}
function testTwoAccountsOpenAtSameCloseAtDifferent() external {
console.log("deposit collateral A");
depositToVault(initialCollateral, userA);
depositToVault(initialCollateral, userB);
console.log("userA shares", UFixed18ToUint(vault.balanceOf(userA)));
console.log("userB shares", UFixed18ToUint(vault.balanceOf(userB)));
console.log("Total shares", UFixed18ToUint(vault.totalSupply()));
console.log("=====================================================\n");
console.log("UserA redeems");
tcapOracle.nextSame();
vault.sync();
console.log("userA shares", UFixed18ToUint(vault.balanceOf(userA)));
console.log("userB shares", UFixed18ToUint(vault.balanceOf(userB)));
console.log("Total shares", UFixed18ToUint(vault.totalSupply()));
vm.startPrank(userA);
vault.redeem(vault.maxRedeem(userA), userA);
vm.stopPrank();
console.log("userA shares", UFixed18ToUint(vault.balanceOf(userA)));
console.log("userB shares", UFixed18ToUint(vault.balanceOf(userB)));
console.log("Total shares", UFixed18ToUint(vault.totalSupply()));
console.log("fees", UFixed18ToUint(collateral.fees(cryptexTreasury)));
console.log("=====================================================\n");
console.log("UserA claim");
tcapOracle.nextSame();
vault.sync();
console.log("DSU balance UserA", DSU.balanceOf(userA));
vm.startPrank(userA);
vault.claim(userA);
vm.stopPrank();
console.log("DSU balance UserA", DSU.balanceOf(userA));
console.log("userA shares", UFixed18ToUint(vault.balanceOf(userA)));
console.log("userB shares", UFixed18ToUint(vault.balanceOf(userB)));
console.log("Total shares", UFixed18ToUint(vault.totalSupply()));
console.log("fees", UFixed18ToUint(collateral.fees(cryptexTreasury)));
console.log("=====================================================\n");
console.log("UserB redeems");
tcapOracle.nextSame();
vault.sync();
vm.startPrank(userB);
vault.redeem(vault.maxRedeem(userB), userB);
vm.stopPrank();
console.log("userA shares", UFixed18ToUint(vault.balanceOf(userA)));
console.log("userB shares", UFixed18ToUint(vault.balanceOf(userB)));
console.log("Total shares", UFixed18ToUint(vault.totalSupply()));
console.log("=====================================================\n");
console.log("UserB claim");
tcapOracle.nextSame();
vault.sync();
vm.startPrank(userB);
// the statement below vault.claim will fail
vault.claim(userB);
vm.stopPrank();
console.log("userA shares", UFixed18ToUint(vault.balanceOf(userA)));
console.log("userB shares", UFixed18ToUint(vault.balanceOf(userB)));
console.log("Total shares", UFixed18ToUint(vault.totalSupply()));
console.log("=====================================================\n");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment