Last active
July 2, 2023 15:34
-
-
Save voith/96a2a0b495d7987afa88b31d7b9f8062 to your computer and use it in GitHub Desktop.
This file contains hidden or 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: 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; | |
} | |
} |
This file contains hidden or 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: 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)); | |
} | |
} |
This file contains hidden or 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: 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; | |
} | |
} |
This file contains hidden or 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: 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(); | |
_; | |
} | |
} |
This file contains hidden or 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: 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; | |
} | |
} |
This file contains hidden or 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: 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); | |
} | |
} |
This file contains hidden or 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: 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