├── .gas-snapshot ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── .solhint.json ├── Makefile ├── README.md ├── foundry.toml ├── notes.txt ├── remappings.txt ├── script ├── CalculateSyncGasCosts.sol ├── Deploy.Goerli.s.sol ├── Deploy.Mainnet.s.sol ├── Deployment.sol ├── Parameters.sol └── ReadData.Goerli.s.sol ├── slither.config.json ├── src ├── core │ ├── Dyad.sol │ └── dNFT.sol ├── interfaces │ ├── AggregatorV3Interface.sol │ └── IdNFT.sol └── stake │ └── Staking.sol ├── test ├── Launch.t.sol ├── Oracle.t.sol ├── Pool.Eike.t.sol ├── Pool.t.sol ├── dNFT.t.sol ├── dyad.t.sol ├── interfaces │ └── ICheatCodes.sol ├── stake │ └── Staking.t.sol └── util │ └── Util.sol └── util ├── addresses.py ├── gas.py └── gas_deployment.py /.gas-snapshot: -------------------------------------------------------------------------------- 1 | PoolTest:testSync() (gas: 853482) 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | broadcast/* 10 | 11 | # Dotenv file 12 | .env 13 | 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts"] 5 | path = lib/openzeppelin-contracts 6 | url = https://github.com/openzeppelin/openzeppelin-contracts 7 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:default" 3 | } 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include .env 2 | 3 | ifdef FILE 4 | matchFile = --match-contract $(FILE) 5 | endif 6 | ifdef FUNC 7 | matchFunction = --match $(FUNC) 8 | endif 9 | 10 | t: 11 | forge test $(matchFile) $(matchFunction) -vv --fork-url $(RPC) 12 | tt: 13 | forge test $(matchFile) $(matchFunction) -vvv --fork-url $(RPC) 14 | ttt: 15 | forge test $(matchFile) $(matchFunction) -vvvv --fork-url $(RPC) 16 | 17 | lt: 18 | forge test $(matchFile) $(matchFunction) -vv 19 | ltt: 20 | forge test $(matchFile) $(matchFunction) -vvv 21 | lttt: 22 | forge test $(matchFile) $(matchFunction) -vvvv 23 | 24 | gas-report: 25 | forge test --gas-report --fork-url $(RPC) 26 | 27 | anvil: 28 | anvil --fork-url $(RPC) --chain-id 1337 --block-time 5 29 | 30 | # deploy on Locally forked mainnet 31 | ldeploy: 32 | forge script script/Deploy.Mainnet.s.sol --rpc-url http://localhost:8545 --chain-id 1337 --sender 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --broadcast -i 1 33 | 34 | # deploy on goerli 35 | gdeploy: 36 | forge script script/Deploy.Goerli.s.sol --rpc-url $(GOERLI_RPC) --sender $(PUBLIC_KEY) --broadcast --verify -i 1 -vvvv 37 | 38 | # deploy on forked mainnet 39 | deploy: 40 | forge script script/Deploy.Mainnet.s.sol --rpc-url $(RPC) --chain-id 1 --sender 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --broadcast -i 1 41 | 42 | calc-deployment-gas-fees: 43 | p gas.py --gas $(p gas_deployment.py) 44 | 45 | calc-sync-gas-fees: 46 | forge script script/CalculateSyncGasCosts.sol --fork-url $(RPC) 47 | 48 | read-data: 49 | forge script script/ReadData.Goerli.s.sol --fork-url $(GOERLI_RPC) 50 | 51 | slither: 52 | slither . 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DYAD 2 | 3 | ![dyad](https://pbs.twimg.com/profile_images/1580864472079532032/uCLwW3nb_200x200.jpg) 4 | 5 | This repo contains the smart contracts for the dyad protocol. 6 | 7 | ## Run 8 | 9 | 1) Install [foundry](https://book.getfoundry.sh/getting-started/installation) 10 | 2) Run `forge build` 11 | 12 | ## Test 13 | 14 | ``` 15 | forge test --fork-url {MAINNET_RPC} 16 | ``` 17 | 18 | ## Deploy 19 | 20 | ``` 21 | forge script script/Deploy.Mainnet.s.sol --rpc-url ${RPC} --chain-id 1 --sender ${SENDER} --broadcast -i 1 22 | ``` 23 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'src' 3 | out = 'out' 4 | libs = ['lib'] 5 | 6 | # MAKES COMPILATION VERY SLOW!! 7 | # ir optimizer 8 | # viaIR = true 9 | optimizer_runs = 200 10 | 11 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config 12 | 13 | [rpc_endpoints] 14 | goerli = "${GOERLI_RPC_URL}" 15 | 16 | [etherscan] 17 | goerli = { key = "${ETHERSCAN_API_KEY}" } 18 | -------------------------------------------------------------------------------- /notes.txt: -------------------------------------------------------------------------------- 1 | eth at 1.3k = 130000000000 2 | eth at 1.2k = 120000000000 3 | 4 | 120148000000 5 | 6 | chainlink oracle mainnet: 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 7 | chainlink oracle goerli: 0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e 8 | 9 | ANVIL PRIVATE_KEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 10 | 11 | TODO: 12 | - [X] Remove balance because we can not keep track of it 13 | Balance is more like a virtual thing 14 | This should be called withdrawn 15 | - [ ] nft.withdrawn should be balanceOf() -> No I think this is wrong 16 | - [X] when protocol launch we have an xp deadlock 17 | - [X] we need to access the id => nft mapping by tokenByIndex 18 | - [X] add README.md 19 | - [X] rename recipient to receiver 20 | - [X] automatic etherscan verification (should work out of the box) 21 | - [ ] nft.deposit > or >= ? 22 | - [ ] replace 10k with const 23 | - [ ] redeem: get the latest price or lastEthPrice? 24 | - [ ] rename to ehjc 25 | 26 | STAKING 27 | - [ ] add limit 28 | 29 | 30 | GAS SAVINGS: 31 | - [ ] unchecked in loop 32 | - [ ] chache state vars that are used in for loop 33 | 34 | UNCERTAINTIES: 35 | - [ ] can it happen that every nft deposit is negative? 36 | this would make `multiProductsSum` in sync() equal to 0 37 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @openzeppelin/=lib/openzeppelin-contracts/ 2 | -------------------------------------------------------------------------------- /script/CalculateSyncGasCosts.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | import "../src/core/Dyad.sol"; 6 | import {IdNFT} from "../src/interfaces/IdNFT.sol"; 7 | import {dNFT} from "../src/core/dNFT.sol"; 8 | import {Parameters} from "./Parameters.sol"; 9 | import "forge-std/console.sol"; 10 | import {Deployment} from "./Deployment.sol"; 11 | 12 | contract CalculateSyncGasCosts is Script, Parameters { 13 | function run() public { 14 | address dNftAddr; address dyadAddr; 15 | 16 | (dNftAddr, dyadAddr) = new Deployment().deploy( 17 | 0, 18 | MAX_SUPPLY, 19 | BLOCKS_BETWEEN_SYNCS, 20 | MIN_COLLATERIZATION_RATIO, 21 | MAX_MINTED_BY_TVL, 22 | ORACLE_MAINNET, 23 | new address[](0) 24 | ); 25 | IdNFT dnft = IdNFT(dNftAddr); 26 | 27 | for (uint i = 0; i < MAX_SUPPLY; i++) { 28 | dnft.mintNft{value: 5 ether}(address(this)); 29 | } 30 | uint g1 = gasleft(); 31 | dnft.sync(0); 32 | console.log("gas used: ", g1 - gasleft()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /script/Deploy.Goerli.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | import {Deployment} from "./Deployment.sol"; 6 | import {Parameters} from "./Parameters.sol"; 7 | 8 | // Pseudo-code, may not compile. 9 | contract DeployGoerli is Script, Parameters { 10 | function run() public { 11 | new Deployment().deploy( 12 | DEPOSIT_MINIMUM_GOERLI, 13 | MAX_SUPPLY, 14 | BLOCKS_BETWEEN_SYNCS, 15 | MIN_COLLATERIZATION_RATIO, 16 | MAX_MINTED_BY_TVL, 17 | ORACLE_GOERLI, 18 | INSIDERS 19 | ); 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /script/Deploy.Mainnet.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | import {Deployment} from "./Deployment.sol"; 6 | import {Parameters} from "./Parameters.sol"; 7 | 8 | // Run on a local mainnet fork 9 | contract DeployMainnet is Script, Parameters { 10 | function run() public { 11 | new Deployment().deploy( 12 | DEPOSIT_MINIMUM_MAINNET, 13 | MAX_SUPPLY, 14 | BLOCKS_BETWEEN_SYNCS, 15 | MIN_COLLATERIZATION_RATIO, 16 | MAX_MINTED_BY_TVL, 17 | ORACLE_MAINNET, 18 | INSIDERS 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /script/Deployment.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | import {IdNFT} from "../src/interfaces/IdNFT.sol"; 6 | import {dNFT} from "../src/core/dNFT.sol"; 7 | import "../src/core/Dyad.sol"; 8 | import {Parameters} from "../script/Parameters.sol"; 9 | 10 | contract Deployment is Script { 11 | function deploy( 12 | uint depositMinimum, 13 | uint maxSupply, 14 | uint blocksBetweenSyncs, 15 | uint minCollaterizationRatio, 16 | uint maxMintedByTVL, 17 | address oracle, 18 | address[] memory insiders 19 | ) public returns (address, address) { 20 | vm.startBroadcast(); 21 | DYAD dyad = new DYAD(); 22 | 23 | dNFT _dnft = new dNFT(address(dyad), 24 | depositMinimum, 25 | maxSupply, 26 | blocksBetweenSyncs, 27 | minCollaterizationRatio, 28 | maxMintedByTVL, 29 | oracle, 30 | insiders); 31 | 32 | IdNFT dnft = IdNFT(address(_dnft)); 33 | 34 | dyad.transferOwnership(address(dnft)); 35 | 36 | vm.stopBroadcast(); 37 | 38 | return (address(dnft), address(dyad)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /script/Parameters.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | contract Parameters { 5 | uint MAX_SUPPLY = 500; 6 | uint MIN_COLLATERIZATION_RATIO = 15000; 7 | uint BLOCKS_BETWEEN_SYNCS = 1; 8 | uint MAX_MINTED_BY_TVL = 50000; // 500% 9 | 10 | // mainnet 11 | address ORACLE_MAINNET = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; 12 | uint DEPOSIT_MINIMUM_MAINNET = 5000000000000000000000; // $5k deposit minimum 13 | 14 | // goerli 15 | address ORACLE_GOERLI = 0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e; 16 | uint DEPOSIT_MINIMUM_GOERLI = 1000000000000000000; // $1 deposit minimum 17 | 18 | address[] INSIDERS = [ 19 | 0x7EEfFd5D089b1351ecCC388022d8b823676dF424, // cryptohermetica 20 | 0xCAD2EaDA97Ad393584Fe84A5cCA1ef3093E45ae4, // joeyroth.eth 21 | 0x414b60745072088d013721b4a28a0559b1A9d213, // shafu.eth 22 | 0x3682827F48F8E023EE40707dEe82620D0B63579f, // Max Entropy 23 | 0xe779Fb090AF9dfBB3b4C18Ed571ad6390Df52ae2, // dma.eth 24 | 0x9F919a292e62594f2D8db13F6A4ADB1691D6c60d, // kores 25 | 0x1b8afB86A36134691Ef9AFba90F143d9b5e8aBbB, // e_z.eth 26 | 0xe9fC93E678F2Bde7A0a3bA3d39F505Ef63a68C97, // ehjc 27 | 0x78965cecb4696165B374FeA43Bac3029006Dec2c, // 0xMurathan 28 | 0xE264df996EF2934b8134AA1A03354F1FCd547939 // Ziad 29 | ]; 30 | } 31 | -------------------------------------------------------------------------------- /script/ReadData.Goerli.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | 6 | contract ReadData is Script { 7 | 8 | function run() public { 9 | // pool = Pool(0xAf593430b86a0560818a9dF5858B14dDC469Ab98); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /slither.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "filter_paths": "lib", 3 | "solc_remaps": [ 4 | "ds-test/=lib/ds-test/src/", 5 | "forge-std/=lib/forge-std/src/" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/core/Dyad.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | 7 | contract DYAD is ERC20, Ownable { 8 | constructor() ERC20("DYAD Stablecoin", "DYAD") {} 9 | 10 | function mint(address to, uint amount) public onlyOwner { _mint(to, amount); } 11 | function burn(address from, uint amount) public onlyOwner { _burn(from, amount); } 12 | } 13 | -------------------------------------------------------------------------------- /src/core/dNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; 5 | import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 6 | import "@openzeppelin/contracts/utils/math/SafeCast.sol"; 7 | import "@openzeppelin/contracts/utils/math/SignedMath.sol"; 8 | 9 | import {IAggregatorV3} from "../interfaces/AggregatorV3Interface.sol"; 10 | import {DYAD} from "./Dyad.sol"; 11 | 12 | contract dNFT is ERC721Enumerable, ReentrancyGuard { 13 | using SafeCast for int256; 14 | using SafeCast for uint256; 15 | using SignedMath for int256; 16 | 17 | uint public immutable DEPOSIT_MINIMUM; // Min DYAD required to mint a new dNFT 18 | uint public immutable MAX_SUPPLY; // Max number of dNFTs that can exist simultaneously 19 | uint public immutable BLOCKS_BETWEEN_SYNCS; // Min number of blocks required between sync calls 20 | uint public immutable MIN_COLLATERIZATION_RATIO; // Min CR required to withdraw DYAD 21 | uint public immutable MAX_MINTED_BY_TVL; // Max % of DYAD that can be minted by TVL 22 | 23 | uint public lastEthPrice; // ETH price from the last sync call 24 | uint public lastSyncedBlock; // Last block sync was called on 25 | uint public minXp; // Min XP over all dNFTs 26 | uint public maxXp; // Max XP over all dNFTs 27 | 28 | mapping(uint => Nft) public idToNft; // dNFT id => dNFT 29 | mapping(uint => uint) private _idToBlockOfLastDeposit; // dNFT id => Block deposit was called on 30 | 31 | struct Nft { 32 | uint withdrawn; // dyad withdrawn from the pool 33 | int deposit; // dyad balance in pool 34 | uint xp; // always positive, always inflationary 35 | bool isLiquidatable; // if true, anyone can liquidate the dNFT 36 | } 37 | 38 | // Convenient way to store output of internal `calcMulti` functions 39 | struct Multi { uint product ; uint xp; } 40 | struct Multis { uint[] products; uint productsSum; uint[] xps; } 41 | 42 | bytes private constant XP_TO_MULTI = hex"333333333435353637393a3c3f42454a4f555c636c76808b96a0abb5bfc8cfd6dce1e6e9eceff1f2"; 43 | 44 | DYAD public dyad; 45 | IAggregatorV3 internal oracle; 46 | 47 | enum Mode { 48 | BURNING, // Price of ETH went down 49 | MINTING // Price of ETH went up 50 | } 51 | 52 | event NftMinted (address indexed to, uint indexed id); 53 | event DyadMinted (address indexed to, uint indexed id, uint amount); 54 | event DyadWithdrawn(address indexed to, uint indexed id, uint amount); 55 | event DyadDeposited(address indexed to, uint indexed id, uint amount); 56 | event DyadRedeemed (address indexed to, uint indexed id, uint amount); 57 | event DyadMoved (uint indexed from, uint indexed to, uint amount); 58 | event NftLiquidated(address indexed from, address indexed to, uint indexed id); 59 | event Synced (uint id); 60 | 61 | error ReachedMaxSupply (); 62 | error NoEthSupplied (); 63 | error SyncedTooRecently (); 64 | error ExceedsAverageTVL (); 65 | error NotNFTOwner (uint id); 66 | error NotLiquidatable (uint id); 67 | error CrTooLow (uint cr); 68 | error AmountZero (uint amount); 69 | error NotReachedMinAmount (uint amount); 70 | error ExceedsWithdrawalLimit (uint amount); 71 | error ExceedsDepositLimit (uint amount); 72 | error AddressZero (address addr); 73 | error FailedDyadTransfer (address to, uint amount); 74 | error FailedEthTransfer (address to, uint amount); 75 | error CannotMoveDepositToSelf(uint from, uint to, uint amount); 76 | error MinXpHigherThanMaxXp (uint minXp, uint maxXp); 77 | error CannotDepositAndWithdrawInSameBlock(); 78 | 79 | modifier onlyNFTOwner(uint id) { 80 | if (ownerOf(id) != msg.sender) revert NotNFTOwner(id); _; 81 | } 82 | modifier amountNotZero(uint amount) { 83 | if (amount == 0) revert AmountZero(amount); _; 84 | } 85 | modifier addressNotZero(address addr) { 86 | if (addr == address(0)) revert AddressZero(addr); _; 87 | } 88 | 89 | constructor( 90 | address _dyad, 91 | uint _depositMinimum, 92 | uint _maxSupply, 93 | uint _blocksBetweenSyncs, 94 | uint _minCollaterizationRatio, 95 | uint _maxMintedByTVL, 96 | address _oracle, 97 | address[] memory _insiders 98 | ) ERC721("DYAD NFT", "dNFT") { 99 | dyad = DYAD(_dyad); 100 | oracle = IAggregatorV3(_oracle); 101 | lastEthPrice = _getLatestEthPrice(); 102 | DEPOSIT_MINIMUM = _depositMinimum; 103 | MAX_SUPPLY = _maxSupply; 104 | BLOCKS_BETWEEN_SYNCS = _blocksBetweenSyncs; 105 | MIN_COLLATERIZATION_RATIO = _minCollaterizationRatio; 106 | MAX_MINTED_BY_TVL = _maxMintedByTVL; 107 | minXp = _maxSupply; 108 | maxXp = _maxSupply << 1; // *2 109 | 110 | for (uint id = 0; id < _insiders.length; ) { 111 | _mintNftWithXp(_insiders[id], id); 112 | unchecked { ++id; } 113 | } 114 | } 115 | 116 | // Mint new dNFT to `to` with a deposit of atleast `DEPOSIT_MINIMUM` 117 | function mintNft(address to) external addressNotZero(to) payable returns (uint) { 118 | uint id = totalSupply(); 119 | _mintNftWithXp(to, id); 120 | _mintDyad(id, DEPOSIT_MINIMUM); 121 | return id; 122 | } 123 | 124 | // Mint new dNFT to `to` with `id` id and add Xp if `addXp` is true 125 | function _mintNft( 126 | address to, 127 | uint id 128 | ) private { 129 | if (id >= MAX_SUPPLY) { revert ReachedMaxSupply(); } 130 | _mint(to, id); 131 | emit NftMinted(to, id); 132 | } 133 | 134 | // Call `mintNft` and add xp to the newly minted dNFT 135 | function _mintNftWithXp( 136 | address to, 137 | uint id 138 | ) private { 139 | _mintNft(to, id); 140 | unchecked { 141 | uint xp = (MAX_SUPPLY<<1) - id; // id is always between 0 and MAX_SUPPLY-1 142 | idToNft[id].xp = xp; // break xp symmetry 143 | if (xp < minXp) { minXp = xp; } // sync could have increased `minXp` 144 | } 145 | } 146 | 147 | // Mint and deposit DYAD into dNFT 148 | function mintDyad( 149 | uint id 150 | ) payable public onlyNFTOwner(id) returns (uint amount) { 151 | amount = _mintDyad(id, 0); 152 | } 153 | 154 | // Mint at least `minAmount` of DYAD to dNFT 155 | function _mintDyad( 156 | uint id, 157 | uint minAmount 158 | ) private returns (uint) { 159 | if (msg.value == 0) { revert NoEthSupplied(); } 160 | uint newDyad = msg.value/100000000 * _getLatestEthPrice(); 161 | if (newDyad == 0) { revert AmountZero(newDyad); } 162 | if (newDyad < minAmount) { revert NotReachedMinAmount(newDyad); } 163 | dyad.mint(address(this), newDyad); 164 | idToNft[id].deposit += newDyad.toInt256(); 165 | emit DyadMinted(msg.sender, id, newDyad); 166 | return newDyad; 167 | } 168 | 169 | // Deposit `amount` of DYAD into dNFT 170 | function deposit( 171 | uint id, 172 | uint amount 173 | ) external amountNotZero(amount) returns (uint) { 174 | _idToBlockOfLastDeposit[id] = block.number; 175 | Nft storage nft = idToNft[id]; 176 | if (amount > nft.withdrawn) { revert ExceedsWithdrawalLimit(amount); } 177 | nft.deposit += amount.toInt256(); 178 | nft.withdrawn -= amount; 179 | bool success = dyad.transferFrom(msg.sender, address(this), amount); 180 | if (!success) { revert FailedDyadTransfer(address(this), amount); } 181 | emit DyadDeposited(msg.sender, id, amount); 182 | return amount; 183 | } 184 | 185 | // Withdraw `amount` of DYAD from dNFT 186 | function withdraw( 187 | uint id, 188 | uint amount 189 | ) external onlyNFTOwner(id) amountNotZero(amount) returns (uint) { 190 | if (_idToBlockOfLastDeposit[id] == block.number) { 191 | revert CannotDepositAndWithdrawInSameBlock(); } // stops flash loan attacks 192 | Nft storage nft = idToNft[id]; 193 | if (amount.toInt256() > nft.deposit) { revert ExceedsDepositLimit(amount); } 194 | uint collatVault = address(this).balance/100000000 * _getLatestEthPrice(); // in USD 195 | uint totalWithdrawn = dyad.totalSupply() - dyad.balanceOf(address(this)) + amount; 196 | uint collatRatio = collatVault*10000 / totalWithdrawn; // in bps 197 | if (collatRatio < MIN_COLLATERIZATION_RATIO) { revert CrTooLow(collatRatio); } 198 | uint newWithdrawn = nft.withdrawn + amount; 199 | uint averageTVL = dyad.balanceOf(address(this)) / totalSupply(); 200 | if (newWithdrawn > averageTVL) { revert ExceedsAverageTVL(); } 201 | nft.withdrawn = newWithdrawn; 202 | nft.deposit -= amount.toInt256(); 203 | bool success = dyad.transfer(msg.sender, amount); 204 | if (!success) { revert FailedDyadTransfer(msg.sender, amount); } 205 | emit DyadWithdrawn(msg.sender, id, amount); 206 | return amount; 207 | } 208 | 209 | // Redeem `amount` of DYAD for ETH from dNFT 210 | function redeem( 211 | uint id, 212 | uint amount 213 | ) external nonReentrant() onlyNFTOwner(id) amountNotZero(amount) returns (uint) { 214 | Nft storage nft = idToNft[id]; 215 | if (amount > nft.withdrawn) { revert ExceedsWithdrawalLimit(amount); } 216 | nft.withdrawn -= amount; 217 | dyad.burn(msg.sender, amount); 218 | uint eth = amount*100000000 / lastEthPrice; 219 | (bool success, ) = payable(msg.sender).call{value: eth}(""); 220 | if (!success) { revert FailedEthTransfer(msg.sender, eth); } 221 | emit DyadRedeemed(msg.sender, id, amount); 222 | return eth; 223 | } 224 | 225 | // Move `amount` `from` one dNFT deposit `to` another dNFT deposit 226 | function moveDeposit( 227 | uint _from, 228 | uint _to, 229 | uint amount 230 | ) external onlyNFTOwner(_from) amountNotZero(amount) returns (uint) { 231 | if (_from == _to) { revert CannotMoveDepositToSelf(_from, _to, amount); } 232 | Nft storage from = idToNft[_from]; 233 | if (amount.toInt256() > from.deposit) { revert ExceedsDepositLimit(amount); } 234 | Nft storage to = idToNft[_to]; 235 | from.deposit -= amount.toInt256(); 236 | to.deposit += amount.toInt256(); 237 | emit DyadMoved(_from, _to, amount); 238 | return amount; 239 | } 240 | 241 | // Liquidate dNFT by burning it and minting a new copy to `to` 242 | function liquidate( 243 | uint id, 244 | address to 245 | ) external addressNotZero(to) payable returns (uint) { 246 | Nft memory nft = idToNft[id]; 247 | if (!nft.isLiquidatable) { revert NotLiquidatable(id); } 248 | address owner = ownerOf(id); 249 | _burn(id); 250 | delete idToNft[id]; 251 | _mintCopy(to, nft, id); 252 | emit NftLiquidated(owner, to, id); 253 | return id; 254 | } 255 | 256 | // Mint nft with `id` to `to` with the same xp and withdrawn amount as `nft` 257 | function _mintCopy( 258 | address to, 259 | Nft memory nft, 260 | uint id 261 | ) private returns (uint) { 262 | _mintNft(to, id); 263 | Nft storage newNft = idToNft[id]; 264 | uint minDeposit; 265 | if (nft.deposit < 0) { minDeposit = nft.deposit.abs(); } 266 | int newDeposit = _mintDyad(id, minDeposit).toInt256(); 267 | newNft.deposit = newDeposit + nft.deposit; 268 | newNft.xp = nft.xp; 269 | newNft.withdrawn = nft.withdrawn; 270 | return id; 271 | } 272 | 273 | // Sync by minting/burning DYAD to keep the peg and update each dNFT. 274 | // dNFT with `id` gets a boost. 275 | function sync(uint id) public { 276 | if (block.number < lastSyncedBlock + BLOCKS_BETWEEN_SYNCS) { 277 | revert SyncedTooRecently(); 278 | } 279 | lastSyncedBlock = block.number; 280 | uint newEthPrice = _getLatestEthPrice(); 281 | Mode mode = newEthPrice > lastEthPrice ? Mode.MINTING : Mode.BURNING; 282 | uint ethPriceDelta = newEthPrice*10000 / lastEthPrice; 283 | mode == Mode.MINTING ? ethPriceDelta -= 10000 // in bps 284 | : ethPriceDelta = 10000 - ethPriceDelta; // in bps 285 | uint dyadTotalSupply = dyad.totalSupply(); 286 | uint dyadDelta = dyadTotalSupply*ethPriceDelta / 10000; // percentagOf in bps 287 | if (dyadDelta == 0) { return; } 288 | _updateNFTs(dyadDelta, dyadTotalSupply, mode, id); 289 | mode == Mode.MINTING ? dyad.mint(address(this), dyadDelta) 290 | : dyad.burn(address(this), dyadDelta); 291 | lastEthPrice = newEthPrice; 292 | emit Synced(id); 293 | } 294 | 295 | function _updateNFTs( 296 | uint dyadDelta, 297 | uint dyadTotalSupply, 298 | Mode mode, 299 | uint id 300 | ) private { 301 | uint nftTotalSupply = totalSupply(); 302 | Multis memory multis = _calcMultis(mode, id, nftTotalSupply, dyadTotalSupply); 303 | uint _minXp = type(uint256).max; // local min 304 | uint _maxXp = maxXp; // local max 305 | uint productsSum = multis.productsSum; // saves gas 306 | if (productsSum == 0) { productsSum = 1; } // to avoid dividing by 0 307 | 308 | for (uint i = 0; i < nftTotalSupply; ) { 309 | uint relativeDyadDelta = dyadDelta * // percentagOf in bps 310 | (multis.products[i]*10000 / productsSum) / 10000; // relativeMulti 311 | Nft storage nft = idToNft[i]; 312 | int _deposit = nft.deposit; // save gas 313 | uint _xp = nft.xp; // save gas 314 | 315 | if (mode == Mode.BURNING) { 316 | if (_deposit >= 1) { // if deposit > 0 317 | uint xpAccrual = relativeDyadDelta*100 / (multis.xps[i]); 318 | if (id == i) { xpAccrual = xpAccrual << 1; } // boost by *2 319 | _xp += xpAccrual / (10**18); // norm by 18 decimals 320 | } 321 | _deposit -= relativeDyadDelta.toInt256(); 322 | } else { 323 | _deposit += relativeDyadDelta.toInt256(); 324 | } 325 | 326 | _deposit >= 0 ? nft.isLiquidatable = false : nft.isLiquidatable = true; 327 | 328 | if (_xp < _minXp) { _minXp = _xp; } // new local min 329 | if (_xp > _maxXp) { _maxXp = _xp; } // new local max 330 | 331 | nft.deposit = _deposit; 332 | nft.xp = _xp; 333 | unchecked { ++i; } 334 | } 335 | 336 | if (_minXp > _maxXp) { revert MinXpHigherThanMaxXp(_minXp, _maxXp); } 337 | minXp = _minXp; // save new min 338 | maxXp = _maxXp; // save new max 339 | } 340 | 341 | function _calcMultis( 342 | Mode mode, 343 | uint id, 344 | uint nftTotalSupply, 345 | uint dyadTotalSupply 346 | ) private view returns (Multis memory) { 347 | uint productsSum; 348 | uint[] memory products = new uint[](nftTotalSupply); 349 | uint[] memory xps = new uint[](nftTotalSupply); 350 | uint xpDelta = maxXp - minXp; 351 | if (xpDelta == 0) { xpDelta = 1; } // xpDelta min is 1 352 | 353 | for (uint i = 0; i < nftTotalSupply; ) { 354 | Nft memory nft = idToNft[i]; 355 | Multi memory multi; // defaults to 0, 0 356 | if (nft.deposit > 0) { // multis are 0 if deposit <= 0 357 | multi = _calcMulti(mode, nft, nftTotalSupply, dyadTotalSupply, xpDelta); 358 | } 359 | if (id == i && mode == Mode.MINTING) { 360 | multi.product += multi.product*1500 / 10000; // boost by 15% 361 | } 362 | products[i] = multi.product; 363 | productsSum += multi.product; 364 | xps[i] = multi.xp; 365 | unchecked { ++i; } 366 | } 367 | 368 | return Multis(products, productsSum, xps); 369 | } 370 | 371 | function _calcMulti( 372 | Mode mode, 373 | Nft memory nft, 374 | uint nftTotalSupply, 375 | uint dyadTotalSupply, 376 | uint xpDelta 377 | ) private view returns (Multi memory) { 378 | uint _deposit = nft.deposit.toUint256(); 379 | uint mintedByNft = nft.withdrawn + _deposit; 380 | uint mintedByTvl = mintedByNft*10000 / (dyadTotalSupply / nftTotalSupply); // mintedByNft/avgTVL 381 | if (mintedByTvl > MAX_MINTED_BY_TVL && mode == Mode.BURNING) { 382 | mintedByTvl = MAX_MINTED_BY_TVL; 383 | } 384 | uint xpScaled = ((nft.xp-minXp)*10000 / xpDelta) / 100; 385 | uint xpMulti = 50; // if 0 <= x <= 60, xp multi is 50 386 | unchecked { 387 | if (xpScaled >= 61) { xpMulti = uint(uint8(XP_TO_MULTI[xpScaled - 61])); } // xpScaled is >= 61 388 | if (mode == Mode.BURNING) { xpMulti = 300-xpMulti; } // xpMulti is <= 242 389 | } 390 | uint multiProduct = xpMulti * (mode == Mode.BURNING 391 | ? mintedByTvl 392 | : (_deposit*10000) / (mintedByNft+1)); // depositMulti 393 | return Multi(multiProduct, xpMulti); 394 | } 395 | 396 | // ETH price in USD 397 | function _getLatestEthPrice() internal view returns (uint) { 398 | ( , int newEthPrice, , , ) = oracle.latestRoundData(); 399 | return newEthPrice.toUint256(); 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /src/interfaces/AggregatorV3Interface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | // This is the chainlink interface that we use to get the ETH price 5 | interface IAggregatorV3 { 6 | function decimals() external view returns (uint8); 7 | function description() external view returns (string memory); 8 | function version() external view returns (uint256); 9 | function getRoundData(uint80 _roundId) 10 | external 11 | view 12 | returns ( 13 | uint80 roundId, 14 | int256 answer, 15 | uint256 startedAt, 16 | uint256 updatedAt, 17 | uint80 answeredInRound 18 | ); 19 | function latestRoundData() 20 | external 21 | view 22 | returns ( 23 | uint80 roundId, 24 | int256 answer, 25 | uint256 startedAt, 26 | uint256 updatedAt, 27 | uint80 answeredInRound 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/interfaces/IdNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | interface IdNFT { 5 | struct Nft { 6 | // dyad withdrawn from the pool deposit 7 | uint withdrawn; 8 | // dyad balance in pool 9 | int deposit; 10 | // always positive, always inflationary 11 | uint xp; 12 | // if true the dNFT is open to be liquidatable 13 | bool isLiquidatable; 14 | } 15 | 16 | /** 17 | * @notice Get dNFT by id 18 | * @param id dNFT id 19 | * @return dNFT 20 | */ 21 | function idToNft( 22 | uint id 23 | ) external view returns (Nft memory); 24 | 25 | /** 26 | * @notice Mint a new dNFT 27 | * @dev Will revert: 28 | * - If `msg.value` worth of DYAD < `DEPOSIT_MINIMUM` 29 | * - If total supply of dNFTs is >= `MAX_SUPPLY` 30 | * @dev Emits: 31 | * - NftMinted 32 | * - DyadMinted 33 | * @param to The address to mint the dNFT to 34 | * @return id Id of the new dNFT 35 | */ 36 | function mintNft( 37 | address to 38 | ) external payable returns (uint id); 39 | 40 | /** 41 | * @notice Mint and deposit new DYAD into dNFT 42 | * @dev Will revert: 43 | * - If dNFT is not owned by `msg.sender` 44 | * - If `amount` minted is 0 45 | * @dev Emits: 46 | * - DyadMinted 47 | * @param id Id of the dNFT 48 | * @return amount Amount minted 49 | */ 50 | function mintDyad( 51 | uint id 52 | ) external payable returns (uint); 53 | 54 | /** 55 | * @notice Withdraw `amount` of DYAD from dNFT 56 | * @dev Will revert: 57 | * - If dNFT is not owned by `msg.sender` 58 | * - If `amount` is 0 59 | * - If deposit call for `id` happened in the same block 60 | * - If `amount` is > than dNFT deposit 61 | * - If CR is < `MIN_COLLATERIZATION_RATIO` after withdrawl 62 | * - If new withdrawl amount of dNFT > average tvl 63 | * - If dyad transfer fails 64 | * @dev Emits: 65 | * - DyadWithdrawn 66 | * @param id Id of the dNFT 67 | * @param amount Amount of DYAD to withdraw 68 | * @return amount Amount withdrawn 69 | */ 70 | function withdraw( 71 | uint id, 72 | uint amount 73 | ) external returns (uint); 74 | 75 | /** 76 | * @notice Deposit `amount` of DYAD into dNFT 77 | * @dev Will revert: 78 | * - If dNFT is not owned by `msg.sender` 79 | * - If `amount` is 0 80 | * - If `amount` is > than dNFT withdrawls 81 | * - If dyad transfer fails 82 | * @dev Emits: 83 | * - DyadDeposited 84 | * @param id Id of the dNFT 85 | * @param amount Amount of DYAD to withdraw 86 | * @return amount Amount deposited 87 | */ 88 | function deposit(uint id, uint amount) external returns (uint); 89 | 90 | /** 91 | * @notice Redeem `amount` of DYAD for ETH from dNFT 92 | * @dev Will revert: 93 | * - If dNFT is not owned by `msg.sender` 94 | * - If `amount` is 0 95 | * - If `amount` is > than dNFT withdrawls 96 | * @dev Emits: 97 | * - DyadRedeemed 98 | * @param id Id of the dNFT 99 | * @param amount Amount of DYAD to redeem 100 | * @return amount Amount of ETH redeemed 101 | */ 102 | function redeem(uint id, uint amount) external returns (uint); 103 | 104 | /** 105 | * @notice Move `amount` `from` one dNFT deposit `to` another dNFT deposit 106 | * @dev Will revert: 107 | * - If `from` dNFT is not owned by `msg.sender` 108 | * - If `amount` is 0 109 | * - If `from` == `to` 110 | * - If `amount` is > than `from` dNFT deposit 111 | * @dev Emits: 112 | * - DyadMoved 113 | * @param from Id of the dNFT to move the deposit from 114 | * @param to Id of the dNFT to move the deposit to 115 | * @param amount Amount of DYAD to move 116 | * @return amount Amount of ETH redeemed 117 | */ 118 | function moveDeposit(uint from, uint to, uint amount) external returns (uint); 119 | 120 | /** 121 | * @notice Liquidate dNFT by burning it and minting a new copy to `to`. Copies 122 | * over the burned dNFT xp and withdrawls. The new dNFT deposit will equivalent 123 | * to `msg.value` worth of DYAD. 124 | * @dev Deletes the state of the dNFT being liquidated 125 | * @dev Will revert: 126 | * - If `to` address is 0 127 | * - If dNFT is not liquidatable 128 | * - If `msg.value` worth of DYAD does not cover deposit of the burned dNFT 129 | * @dev Emits: 130 | * - NftLiquidated 131 | * @param id Id of the dNFT to move the deposit from 132 | * @param to Id of the dNFT to move the deposit to 133 | * @return id Id of the newly minted dNFT 134 | */ 135 | function liquidate(uint id, address to) external payable returns (uint); 136 | 137 | /** 138 | * @notice Sync by minting/burning DYAD to keep the peg and update each dNFT. 139 | * @dev Will revert: 140 | * - If sync was called too soon after the last sync call 141 | * @dev Emits: 142 | * - Synced 143 | * @param id Id of the dNFT that gets a boost 144 | */ 145 | function sync(uint id) external; 146 | 147 | // get min/max XP 148 | function maxXp() external view returns (uint); 149 | function minXp() external view returns (uint); 150 | 151 | // ERC721 152 | function ownerOf(uint tokenId) external view returns (address); 153 | function balanceOf(uint id) external view returns (int); 154 | function totalSupply() external view returns (uint); 155 | function transferFrom(address _from, address _to, uint256 _tokenId) external payable; 156 | 157 | // ERC721Enumerable 158 | function tokenByIndex(uint index) external returns (uint); 159 | 160 | // ERC721Burnable 161 | function burn(uint id) external; 162 | } 163 | 164 | -------------------------------------------------------------------------------- /src/stake/Staking.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | // import {IdNFT} from "../interfaces/IdNFT.sol"; 4 | // import "forge-std/console.sol"; 5 | // import "../../src/core/Dyad.sol"; 6 | 7 | // struct Position { 8 | // address owner; 9 | // uint fee; // fee in basis points 10 | // address feeRecipient; 11 | // uint redemptionLimit; // limit the dnft withdrawn amount can not be below 12 | // uint withdrawalLimit; 13 | // } 14 | 15 | contract Staking { 16 | // IdNFT public dnft; 17 | // DYAD public dyad; 18 | 19 | // mapping (uint => Position) public positions; 20 | 21 | // modifier isPositionOwner(uint id) { 22 | // require(msg.sender == positions[id].owner, "Staking: Not Position Owner"); 23 | // _; 24 | // } 25 | 26 | // constructor(address _dnft, address _dyad) { 27 | // dnft = IdNFT(_dnft); 28 | // dyad = DYAD(_dyad); 29 | // } 30 | 31 | // // is needed, because `dnft.redeem` sends us eth 32 | // receive() external payable {} 33 | 34 | // function stake(uint id, Position memory _position) public returns (Position memory) { 35 | // dnft.transferFrom(_position.owner, address(this), id); 36 | // positions[id] = _position; 37 | // return _position; 38 | // } 39 | 40 | // function unstake(uint id) public isPositionOwner(id) { 41 | // dnft.transferFrom(address(this), positions[id].owner, id); 42 | // delete positions[id]; 43 | // } 44 | 45 | // function setPosition(uint id, Position memory _position) external isPositionOwner(id) { 46 | // positions[id] = _position; 47 | // } 48 | 49 | // // redeem DYAD for ETH 50 | // function redeem(uint id, uint amount) external { 51 | // Position memory _position = positions[id]; 52 | // require(dnft.idToNft(id).withdrawn - amount >= _position.redemptionLimit, 53 | // "Staking: Exceeds Redemption Limit"); 54 | // dyad.transferFrom(msg.sender, address(this), amount); 55 | // dyad.approve(address(dnft), amount); 56 | // uint usdInEth = dnft.redeem(id, amount); 57 | // uint fee = PoolLibrary.percentageOf(usdInEth, _position.fee); 58 | // payable(_position.feeRecipient).transfer(fee); 59 | // payable(msg.sender).transfer(usdInEth - fee); 60 | // } 61 | 62 | // // mint DYAD with ETH 63 | // function mint(uint id) external payable { 64 | // require(msg.value > 0, "Staking: No ETH sent"); 65 | // Position memory _position = positions[id]; 66 | // uint amount = dnft.mintDyad{value: msg.value}(id); 67 | // require(dnft.idToNft(id).withdrawn + amount <= _position.withdrawalLimit, 68 | // "Staking: Exceeds Withdrawl Limit"); 69 | // dyad.approve(address(dnft), amount); 70 | // dnft.withdraw(id, amount); 71 | // uint fee = PoolLibrary.percentageOf(amount, _position.fee); 72 | // dyad.transfer(_position.feeRecipient, fee); 73 | // dyad.transfer(msg.sender, amount - fee); 74 | // } 75 | } 76 | -------------------------------------------------------------------------------- /test/Launch.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "forge-std/console.sol"; 6 | import "../src/core/Dyad.sol"; 7 | import "ds-test/test.sol"; 8 | import {IdNFT} from "../src/interfaces/IdNFT.sol"; 9 | import {dNFT} from "../src/core/dNFT.sol"; 10 | import {OracleMock} from "./Oracle.t.sol"; 11 | import {Parameters} from "../script/Parameters.sol"; 12 | import {Deployment} from "../script/Deployment.sol"; 13 | 14 | interface CheatCodes { 15 | // Gets address for a given private key, (privateKey) => (address) 16 | function addr(uint256) external returns (address); 17 | } 18 | 19 | address constant CHAINLINK_ORACLE_ADDRESS = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; 20 | uint constant DEPOSIT_MINIMUM = 5000000000000000000000; 21 | 22 | // this should simulate the inital lauch on mainnet 23 | // IMPORTANT: you have to run this as a mainnet fork!!! 24 | contract LaunchTest is Test, Parameters, Deployment { 25 | uint NUMBER_OF_INSIDER_NFTS; 26 | 27 | IdNFT public dnft; 28 | DYAD public dyad; 29 | OracleMock public oracle; 30 | 31 | // --------------------- Test Addresses --------------------- 32 | CheatCodes cheats = CheatCodes(HEVM_ADDRESS); 33 | address public addr1; 34 | address public addr2; 35 | 36 | // needed, so we can receive eth transfers 37 | receive() external payable {} 38 | 39 | function setUp() public { 40 | address _dnft; 41 | address _dyad; 42 | (_dnft,_dyad) = deploy( 43 | DEPOSIT_MINIMUM, 44 | MAX_SUPPLY, 45 | BLOCKS_BETWEEN_SYNCS, 46 | MIN_COLLATERIZATION_RATIO, 47 | MAX_MINTED_BY_TVL, 48 | CHAINLINK_ORACLE_ADDRESS, 49 | INSIDERS 50 | ); 51 | dnft = IdNFT(_dnft); 52 | dyad = DYAD(_dyad); 53 | 54 | // directly after deployment the total supply has to be the number 55 | // of insider allocations. 56 | NUMBER_OF_INSIDER_NFTS = dnft.totalSupply(); 57 | 58 | addr1 = cheats.addr(1); vm.deal(addr1, 100 ether); 59 | addr2 = cheats.addr(2); vm.deal(addr2, 100 ether); 60 | } 61 | 62 | function testInsiderAllocation() public { 63 | // we have `NUMBER_OF_INSIDER_NFTS` insiders that we allocate for 64 | assertEq(dnft.totalSupply(), INSIDERS.length); 65 | } 66 | 67 | function testXpOfInsiderNft() public { 68 | assertEq(dnft.idToNft(0).xp, MAX_SUPPLY*2); 69 | assertEq(dnft.idToNft(1).xp, MAX_SUPPLY*2-1); 70 | } 71 | 72 | function testFirstSync() public { 73 | dnft.mintNft{value: 5 ether}(address(this)); 74 | dnft.sync(99999); 75 | } 76 | 77 | function testMintNormallyAndSync() public { 78 | dnft.mintNft{value: 5 ether}(address(this)); 79 | vm.prank(addr1); 80 | dnft.mintNft{value: 5 ether}(address(this)); 81 | vm.prank(addr2); 82 | dnft.mintNft{value: 5 ether}(address(this)); 83 | dnft.mintNft{value: 5 ether}(address(this)); 84 | dnft.sync(99999); 85 | } 86 | 87 | function testWithdrawAndSync() public { 88 | dnft.mintNft{value: 5 ether}(addr1); 89 | vm.prank(addr1); 90 | // remember the nfts are 0 indexed, so we do not need to increment by 1. 91 | dnft.withdraw(NUMBER_OF_INSIDER_NFTS, 4 ether); 92 | 93 | dnft.mintNft{value: 5 ether}(addr2); 94 | vm.prank(addr2); 95 | // remember the nfts are 0 indexed, so we do not need to increment by 1. 96 | dnft.withdraw(NUMBER_OF_INSIDER_NFTS+1, 3 ether); 97 | 98 | dnft.sync(99999); 99 | } 100 | 101 | // very self explanatory I think. Do random stuff and see if it breaks. 102 | // I think you call that fuzzy testing, lol :D 103 | function testDoRandomStuffAndSync() public { 104 | uint currentBlockNumber = block.number; 105 | uint numberOfSyncCalls = 0; 106 | 107 | dnft.mintNft{value: 5 ether}(address(this)); 108 | dnft.sync(99999); 109 | numberOfSyncCalls += 1; 110 | 111 | // mint nfts 112 | uint id1 = dnft.mintNft{value: 5 ether}(address(this)); 113 | vm.prank(addr1); 114 | uint id2 = dnft.mintNft{value: 12 ether}(addr1); 115 | vm.prank(addr1); 116 | uint id3 = dnft.mintNft{value: 5 ether}(addr1); 117 | vm.prank(addr2); 118 | uint id4 = dnft.mintNft{value: 14 ether}(addr2); 119 | vm.prank(addr2); 120 | uint id5 = dnft.mintNft{value: 5 ether}(addr2); 121 | uint id6 = dnft.mintNft{value: 8 ether}(address(this)); 122 | uint id7 = dnft.mintNft{value: 5 ether}(address(this)); 123 | 124 | vm.roll(currentBlockNumber + (numberOfSyncCalls*BLOCKS_BETWEEN_SYNCS)); 125 | dnft.sync(99999); 126 | numberOfSyncCalls += 1; 127 | 128 | // do some withdraws 129 | dnft.withdraw(id1, 2 ether); 130 | dnft.withdraw(id1, 1 ether); 131 | vm.prank(addr1); 132 | dnft.withdraw(id2, 66626626262662); 133 | vm.prank(addr1); 134 | dnft.withdraw(id3, 4 ether); 135 | vm.prank(addr2); 136 | dnft.withdraw(id4, 100000000000); 137 | vm.prank(addr2); 138 | dnft.withdraw(id5, 5 ether); 139 | dnft.withdraw(id6, 2 ether); 140 | dnft.withdraw(id7, 4444444444444); 141 | 142 | vm.roll(currentBlockNumber + (numberOfSyncCalls*BLOCKS_BETWEEN_SYNCS)); 143 | dnft.sync(99999); 144 | numberOfSyncCalls += 1; 145 | 146 | // do some deposits 147 | dyad.approve(address(dnft), 5 ether); 148 | dnft.deposit(id1, 1 ether); 149 | dnft.deposit(id1, 5000000); 150 | vm.prank(addr2); 151 | dyad.approve(address(dnft), 5 ether); 152 | vm.prank(addr2); 153 | dnft.deposit(id4, 1000); 154 | 155 | for(uint i = 0; i < 4; i++) { 156 | vm.roll(currentBlockNumber + (numberOfSyncCalls*BLOCKS_BETWEEN_SYNCS)); 157 | dnft.sync(99999); 158 | numberOfSyncCalls += 1; 159 | } 160 | 161 | // do some redeems 162 | dyad.approve(address(dnft), 5 ether); 163 | dnft.redeem(id1, 100000002); 164 | dnft.redeem(id1, 100000202); 165 | dnft.redeem(id1, 3000000202); 166 | 167 | for(uint i = 0; i < 4; i++) { 168 | vm.roll(currentBlockNumber + (numberOfSyncCalls*BLOCKS_BETWEEN_SYNCS)); 169 | dnft.sync(99999); 170 | numberOfSyncCalls += 1; 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /test/Oracle.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | contract OracleMock { 5 | // NOTE: this value has to be overwritten in the tests 6 | // 7 | // Some examples for quick copy/pasta: 8 | // 95000000 -> - 5% 9 | // 110000000 -> +10% 10 | // 100000000 -> +-0% 11 | int public price = 0; 12 | 13 | function latestRoundData() public view returns ( 14 | uint80 roundId, 15 | int256 answer, 16 | uint256 startedAt, 17 | uint256 updatedAt, 18 | uint80 answeredInRound 19 | ) { 20 | return (1, price, 1, 1, 1); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/Pool.Eike.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "forge-std/console.sol"; 6 | import "../src/core/Dyad.sol"; 7 | import "ds-test/test.sol"; 8 | import {IdNFT} from "../src/interfaces/IdNFT.sol"; 9 | import {dNFT} from "../src/core/dNFT.sol"; 10 | import {OracleMock} from "./Oracle.t.sol"; 11 | import {Parameters} from "../script/Parameters.sol"; 12 | import {Deployment} from "../script/Deployment.sol"; 13 | 14 | uint constant DEPOSIT_MINIMUM = 5000000000000000000000; 15 | 16 | interface CheatCodes { 17 | // Gets address for a given private key, (privateKey) => (address) 18 | function addr(uint256) external returns (address); 19 | } 20 | 21 | // reproduce eikes equations 22 | // https://docs.google.com/spreadsheets/d/1pegDYo8hrOQZ7yZY428F_aQ_mCvK0d701mygZy-P04o/edit#gid=0 23 | // There are many hard coded values here that are based on the equations in the 24 | // google sheet. 25 | contract PoolTest is Test, Parameters, Deployment { 26 | using stdStorage for StdStorage; 27 | 28 | DYAD public dyad; 29 | IdNFT public dnft; 30 | OracleMock public oracle; 31 | 32 | CheatCodes cheats = CheatCodes(HEVM_ADDRESS); 33 | 34 | uint blockNumber; 35 | 36 | function setUp() public { 37 | oracle = new OracleMock(); 38 | 39 | address _dnft; 40 | address _dyad; 41 | (_dnft,_dyad) = deploy( 42 | 77 * 10**8, // DEPOSIT_MINIMUM 43 | MAX_SUPPLY, 44 | BLOCKS_BETWEEN_SYNCS, 45 | MIN_COLLATERIZATION_RATIO, 46 | MAX_MINTED_BY_TVL, 47 | address(oracle), 48 | new address[](0) 49 | ); 50 | dnft = IdNFT(_dnft); 51 | dyad = DYAD(_dyad); 52 | 53 | // set oracle price 54 | vm.store(address(oracle), bytes32(uint(0)), bytes32(uint(950 * 10**8))); 55 | 56 | // mint 10 nfts with a specific deposit to re-create the equations 57 | for (uint i = 0; i < 10; i++) { 58 | dnft.mintNft{value: 10106*(10**15)}(cheats.addr(i+1)); // i+1 to avoid 0x0 address 59 | } 60 | 61 | setNfts(); 62 | 63 | stdstore.target(address(dnft)).sig("lastEthPrice()").checked_write(bytes32(uint(1000 * 10**8))); // min xp 64 | stdstore.target(address(dnft)).sig("minXp()").checked_write(1079); // min xp 65 | stdstore.target(address(dnft)).sig("maxXp()").checked_write(8000); // max xp 66 | 67 | blockNumber = block.number; 68 | } 69 | 70 | // needed, so we can receive eth transfers 71 | receive() external payable {} 72 | 73 | function moveToNextBlock() public { 74 | blockNumber += BLOCKS_BETWEEN_SYNCS; 75 | vm.roll(blockNumber); 76 | } 77 | 78 | // set withdrawn, deposit, xp 79 | // NOTE: I get a slot error for isClaimable so we do not set it here and 80 | // leave it as it is. this seems to be broken for bool rn, see: 81 | // https://github.com/foundry-rs/forge-std/pull/103 82 | function overwriteNft(uint id, uint xp, uint deposit, uint withdrawn) public { 83 | IdNFT.Nft memory nft = dnft.idToNft(id); 84 | nft.withdrawn = withdrawn; nft.deposit = int(deposit); nft.xp = xp; 85 | 86 | stdstore.target(address(dnft)).sig("idToNft(uint256)").with_key(id) 87 | .depth(0).checked_write(nft.withdrawn * 10 ** 18); 88 | stdstore.target(address(dnft)).sig("idToNft(uint256)").with_key(id) 89 | .depth(1).checked_write(uint(nft.deposit) * 10 ** 18); 90 | stdstore.target(address(dnft)).sig("idToNft(uint256)").with_key(id) 91 | .depth(2).checked_write(nft.xp); 92 | // stdstore.target(address(dnft)).sig("idToNft(uint256)").with_key(id) 93 | // .depth(3).checked_write(true); 94 | } 95 | 96 | function setNfts() internal { 97 | // overwrite id, xp, deposit, withdrawn for each nft to the hard-coded 98 | // values in the google sheet 99 | overwriteNft(0, 2161, 146, 3920 ); 100 | overwriteNft(1, 7588, 4616, 7496 ); 101 | overwriteNft(2, 3892, 2731, 10644); 102 | overwriteNft(3, 3350, 4515, 2929 ); 103 | overwriteNft(4, 3012, 2086, 3149 ); 104 | overwriteNft(5, 5496, 7241, 7127 ); 105 | overwriteNft(6, 8000, 8197, 7548 ); 106 | overwriteNft(7, 7000, 5873, 9359 ); 107 | overwriteNft(8, 3435, 1753, 4427 ); 108 | overwriteNft(9, 1079, 2002, 244 ); 109 | } 110 | 111 | // check that the nft deposit values are equal to each other 112 | function assertDeposits(int16[6] memory deposits) internal { 113 | for (uint i = 0; i < deposits.length; i++) { 114 | assertTrue(dnft.idToNft(i).deposit/(10**18) == int(deposits[i])); 115 | } 116 | } 117 | 118 | function triggerBurn() public returns (uint) { 119 | // change new oracle price to something lower so we trigger the burn 120 | vm.store(address(oracle), bytes32(uint(0)), bytes32(uint(950 * 10**8))); 121 | uint totalSupplyBefore = dyad.totalSupply(); 122 | 123 | dnft.sync(99999); 124 | moveToNextBlock(); 125 | 126 | // there should be less dyad now after the sync 127 | assertTrue(totalSupplyBefore > dyad.totalSupply()); 128 | 129 | // dyadDelta 130 | return totalSupplyBefore - dyad.totalSupply(); 131 | } 132 | 133 | function testSyncBurn() public { 134 | uint dyadDelta = triggerBurn(); 135 | assertEq(4800, dyadDelta / (10**18)); 136 | 137 | // check deposits after newly burned dyad. SOME ROUNDING ERRORS! 138 | assertDeposits([-135, 4364, 1804, 3999, 1723, 6249]); 139 | } 140 | 141 | function testSyncBurnWithNegativeDeposit() public { 142 | // after the setup, nft 0 has negative deposit 143 | triggerBurn(); 144 | 145 | dnft.sync(99999); 146 | moveToNextBlock(); 147 | 148 | blockNumber += BLOCKS_BETWEEN_SYNCS; 149 | vm.roll(blockNumber); 150 | 151 | vm.roll(blockNumber + (1*BLOCKS_BETWEEN_SYNCS)); 152 | } 153 | 154 | function testClaim() public { 155 | triggerBurn(); 156 | 157 | // as we can see from the `testSyncBurn` above, the first nft deposit 158 | // is negative (-135), which makes it claimable by others. 159 | 160 | // this is not enough ether to claim the nft 161 | vm.expectRevert(); 162 | dnft.liquidate{value: 1 wei}(0, address(this)); 163 | 164 | vm.expectRevert(); 165 | // 140000000000000000 wei is $133, which is not enough to claim the nft. At 166 | // least 135 is needed. 167 | dnft.liquidate{value: 140000000000000000}(0, address(this)); 168 | 169 | IdNFT.Nft memory liquidatedNft = dnft.idToNft(0); 170 | 171 | // 150000000000000000 wei is $142 in this scenario, which is enough to liquidate 172 | uint id = dnft.liquidate{value: 150000000000000000}(0, address(this)); 173 | 174 | // lets check that all the metadata moved from the burned nft to the newly minted one 175 | assertEq(liquidatedNft.xp, dnft.idToNft(id).xp); 176 | assertEq(liquidatedNft.withdrawn, dnft.idToNft(id).withdrawn); 177 | 178 | // dnft 1 has a positive deposit, and therfore is not claimable 179 | vm.expectRevert(); 180 | dnft.liquidate{value: 1 ether}(1, address(this)); 181 | } 182 | 183 | function triggerMint() public returns (uint) { 184 | // change new oracle price to something higher so we trigger the mint 185 | vm.store(address(oracle), bytes32(uint(0)), bytes32(uint(1100 * 10**8))); 186 | uint totalSupplyBefore = dyad.totalSupply(); 187 | 188 | dnft.sync(99999); 189 | moveToNextBlock(); 190 | 191 | // there should be more dyad now after the sync 192 | assertTrue(totalSupplyBefore < dyad.totalSupply()); 193 | 194 | // dyadDelta 195 | return dyad.totalSupply() - totalSupplyBefore; 196 | } 197 | 198 | function testSyncMint() public { 199 | uint dyadDelta = triggerMint(); 200 | assertEq(9600, dyadDelta/(10**18)); 201 | 202 | // check deposits after newly minted dyad. SOME ROUNDING ERRORS! 203 | // why do we cast the first argument? Good question. This forces 204 | // the compiler to create a int16 array. Is there a better way? 205 | assertDeposits([int16(187), 6593, 2966, 5213, 2544, 7833]); 206 | } 207 | 208 | function testSyncMintBurn() public { triggerMint(); triggerBurn(); } 209 | function testSyncBurnMint() public { triggerBurn(); triggerMint(); } 210 | 211 | function testSyncMintBurnMint() public { triggerMint(); triggerBurn(); triggerMint(); } 212 | function testSyncBurnMintBurn() public { triggerBurn(); triggerMint(); triggerBurn(); } 213 | 214 | function testSyncLiquidation() public { 215 | triggerBurn(); 216 | 217 | // nft 0 is now liquidated, lets claim it! 218 | dnft.liquidate{value: 5 ether}(0, address(this)); 219 | 220 | triggerMint(); 221 | // sync now acts on the newly minted nft, which is a very important test, 222 | // because the newly minted nft has different index from the old one. 223 | dnft.sync(99999); 224 | moveToNextBlock(); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /test/Pool.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "forge-std/console.sol"; 6 | import "../src/core/Dyad.sol"; 7 | import "ds-test/test.sol"; 8 | import {IdNFT} from "../src/interfaces/IdNFT.sol"; 9 | import {dNFT} from "../src/core/dNFT.sol"; 10 | import {OracleMock} from "./Oracle.t.sol"; 11 | import {Deployment} from "../script/Deployment.sol"; 12 | import {Parameters} from "../script/Parameters.sol"; 13 | import {Util} from "./util/Util.sol"; 14 | 15 | uint constant ORACLE_PRICE = 120000000000; // $1.2k 16 | 17 | contract PoolTest is Test, Deployment, Parameters, Util { 18 | IdNFT public dnft; 19 | DYAD public dyad; 20 | OracleMock public oracle; 21 | 22 | function setUp() public { 23 | oracle = new OracleMock(); 24 | setOraclePrice(oracle, ORACLE_PRICE); 25 | 26 | address _dnft; 27 | address _dyad; 28 | (_dnft,_dyad) = deploy( 29 | DEPOSIT_MINIMUM_MAINNET, 30 | MAX_SUPPLY, 31 | BLOCKS_BETWEEN_SYNCS, 32 | MIN_COLLATERIZATION_RATIO, 33 | MAX_MINTED_BY_TVL, 34 | address(oracle), 35 | new address[](0) 36 | ); 37 | dnft = IdNFT(_dnft); 38 | dyad = DYAD(_dyad); 39 | } 40 | 41 | // needed, so we can receive eth transfers 42 | receive() external payable {} 43 | 44 | function testSyncMaxSupply() public { 45 | for (uint i = 0; i < MAX_SUPPLY; i++) { 46 | dnft.mintNft{value: 5 ether}(address(this)); 47 | } 48 | uint gas = gasleft(); 49 | dnft.sync(99999); 50 | console.log("gas used", gas - gasleft()); 51 | } 52 | 53 | function testFailSyncTooSoon() public { 54 | // we need to wait at least `BLOCKS_BETWEEN_SYNCS` blocks between syncs 55 | dnft.sync(99999); 56 | dnft.sync(99999); 57 | } 58 | function testSync() public { 59 | dnft.mintNft{value: 5 ether}(address(this)); 60 | dnft.mintNft{value: 5 ether}(address(this)); 61 | dnft.sync(99999); 62 | vm.roll(block.number + BLOCKS_BETWEEN_SYNCS); 63 | dnft.sync(99999); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/dNFT.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "forge-std/console.sol"; 6 | import "../src/core/Dyad.sol"; 7 | import "ds-test/test.sol"; 8 | import {IdNFT} from "../src/interfaces/IdNFT.sol"; 9 | import {dNFT} from "../src/core/dNFT.sol"; 10 | import {OracleMock} from "./Oracle.t.sol"; 11 | import {Util} from "./util/Util.sol"; 12 | import {Deployment} from "../script/Deployment.sol"; 13 | import {Parameters} from "../script/Parameters.sol"; 14 | 15 | uint constant ORACLE_PRICE = 120000000000; // $1.2k 16 | 17 | contract dNFTTest is Test, Deployment, Parameters, Util { 18 | using stdStorage for StdStorage; 19 | 20 | IdNFT public dnft; 21 | DYAD public dyad; 22 | OracleMock public oracle; 23 | 24 | function setUp() public { 25 | oracle = new OracleMock(); 26 | setOraclePrice(oracle, ORACLE_PRICE); 27 | 28 | address _dnft; 29 | address _dyad; 30 | (_dnft,_dyad) = deploy( 31 | DEPOSIT_MINIMUM_MAINNET, 32 | MAX_SUPPLY, 33 | BLOCKS_BETWEEN_SYNCS, 34 | MIN_COLLATERIZATION_RATIO, 35 | MAX_MINTED_BY_TVL, 36 | address(oracle), 37 | new address[](0) 38 | ); 39 | dnft = IdNFT(_dnft); 40 | dyad = DYAD(_dyad); 41 | } 42 | 43 | // needed, so we can receive eth transfers 44 | receive() external payable {} 45 | 46 | // --------------------- Nft Mint --------------------- 47 | function testMintOneNft() public { 48 | uint id = dnft.mintNft{value: 5 ether}(address(this)); 49 | IdNFT.Nft memory metadata = dnft.idToNft(0); 50 | 51 | uint MAX_XP = MAX_SUPPLY*2; 52 | 53 | assertEq(id, 0); 54 | assertEq(dnft.totalSupply(), 1); 55 | assertEq(metadata.withdrawn, 0); 56 | assertEq(metadata.deposit , int(ORACLE_PRICE*50000000000)); 57 | assertEq(metadata.xp , MAX_XP); 58 | assertEq(dnft.maxXp() , MAX_SUPPLY*2); 59 | 60 | stdstore.target(address(dnft)).sig("minXp()").checked_write(uint(0)); // min xp 61 | stdstore.target(address(dnft)).sig("maxXp()").checked_write(uint(MAX_XP)); // max xp 62 | dnft.sync(99999); 63 | } 64 | function testMintNftTotalSupply() public { 65 | for (uint i = 0; i < 50; i++) { 66 | dnft.mintNft{value: 5 ether}(address(this)); 67 | } 68 | assertEq(dnft.totalSupply(), 50); 69 | } 70 | function testFailMintNftDepositMinimum() public { 71 | // to mint an nft, we need to send 5 ETH 72 | dnft.mintNft{value: 4 ether}(address(this)); 73 | } 74 | function testFailMintNftMaximumSupply() public { 75 | // only `dnft.MAXIMUM_SUPPLY` nfts can be minted 76 | for (uint i = 0; i < MAX_SUPPLY+1; i++) { 77 | dnft.mintNft{value: 5 ether}(address(this)); 78 | } 79 | } 80 | 81 | // --------------------- DYAD Minting --------------------- 82 | function testFailMintDyadNotNftOwner() public { 83 | // only the owner of the nft can mint dyad 84 | dnft.mintNft{value: 5 ether}(address(this)); 85 | vm.prank(address(0)); 86 | dnft.mintDyad{value: 1 ether}(0); 87 | } 88 | function testFailMintDyadWithoutOwner() public { 89 | dnft.mintDyad{value: 1 ether}(99); 90 | } 91 | function testFailMintDyadWithoutEth() public { 92 | // to mint dyad, we need to send ETH > 0 93 | dnft.mintNft{value: 5 ether}(address(this)); 94 | dnft.mintDyad{value: 0 ether}(0); 95 | } 96 | function testMintDyad() public { 97 | dnft.mintNft{value: 5 ether}(address(this)); 98 | dnft.mintDyad{value: 1 ether}(0); 99 | } 100 | function testMintDyadDepositUpdated() public { 101 | dnft.mintNft{value: 5 ether}(address(this)); 102 | dnft.mintDyad{value: 1 ether}(0); 103 | IdNFT.Nft memory metadata = dnft.idToNft(0); 104 | // its 6 ETH because we minted 1 ETH dyad and deposited 5 whilte 105 | // minting the nft. 106 | assertEq(metadata.deposit, int(ORACLE_PRICE*60000000000)); 107 | } 108 | function testMintDyadWithdrawnNotUpdated() public { 109 | dnft.mintNft{value: 5 ether}(address(this)); 110 | dnft.mintDyad{value: 1 ether}(0); 111 | IdNFT.Nft memory metadata = dnft.idToNft(0); 112 | assertEq(metadata.withdrawn, 0); 113 | } 114 | 115 | // --------------------- DYAD Withdraw --------------------- 116 | function testWithdrawDyad() public { 117 | uint AMOUNT_TO_WITHDRAW = 7000000; 118 | dnft.mintNft{value: 5 ether}(address(this)); 119 | dnft.mintDyad{value: 1 ether}(0); 120 | dnft.withdraw(0, AMOUNT_TO_WITHDRAW); 121 | assertEq(dnft.idToNft(0).withdrawn, AMOUNT_TO_WITHDRAW); 122 | assertEq(dnft.idToNft(0).deposit, int(ORACLE_PRICE*60000000000-AMOUNT_TO_WITHDRAW)); 123 | } 124 | function testFailBurnNotdNftContract() public { 125 | uint tokenId = dnft.mintNft{value: 5 ether}(address(this)); 126 | dnft.withdraw(tokenId, 7000000); 127 | dyad.burn(msg.sender, 50); 128 | } 129 | function testFailWithdrawDyadNotNftOwner() public { 130 | dnft.mintNft{value: 5 ether}(address(this)); 131 | vm.prank(address(0)); 132 | dnft.withdraw(0, 7000000); 133 | } 134 | function testFailWithdrawDyadExceedsBalance() public { 135 | dnft.mintNft{value: 5 ether}(address(this)); 136 | // exceeded nft deposit by exactly 1 137 | dnft.withdraw(0, ORACLE_PRICE*50000000000+1); 138 | } 139 | function testFailWithdrawCollaterizationRationTooHigh() public { 140 | dnft.mintNft{value: 5 ether}(address(this)); 141 | dnft.mintDyad{value: 1 ether}(0); 142 | // this pushes the CR over 150% which disables the ability for anyone 143 | // to withdraw more dyad 144 | dnft.withdraw(0, 5000000000000000000000); 145 | dnft.withdraw(0, 2 ether); 146 | } 147 | function testUnblockCollaterizationRatioLock() public { 148 | dnft.mintNft{value: 5 ether}(address(this)); 149 | dnft.mintDyad{value: 1 ether}(0); 150 | uint AMOUNT = 4600000000000000000000; 151 | // this pushes the CR nearly to 150% 152 | dnft.withdraw(0, AMOUNT); 153 | dyad.approve(address(dnft), AMOUNT); 154 | // CR is under 150% so withdraw should fail 155 | vm.expectRevert(); 156 | dnft.withdraw(0, AMOUNT); 157 | // this returns the CR to over 150%, which enables withdrawls again 158 | dnft.deposit(0, AMOUNT); 159 | // we can not deposit+withdraw in same block 160 | vm.roll(block.number + 1); 161 | dnft.withdraw(0, AMOUNT); 162 | } 163 | 164 | // --------------------- DYAD Deposit --------------------- 165 | function testDepositDyad() public { 166 | uint AMOUNT_TO_DEPOSIT = 7000000; 167 | dnft.mintNft{value: 5 ether}(address(this)); 168 | // withdraw dyad -> so we have something to deposit 169 | dnft.withdraw(0, AMOUNT_TO_DEPOSIT); 170 | // we need to approve the dnft contract to spend our dyad 171 | dyad.approve(address(dnft), AMOUNT_TO_DEPOSIT); 172 | dnft.deposit (0, AMOUNT_TO_DEPOSIT); 173 | assertEq(dnft.idToNft(0).withdrawn, 0); 174 | assertEq(dnft.idToNft(0).deposit, int(ORACLE_PRICE*50000000000)); 175 | } 176 | function testFailDepositAndWithdrawInSameBlock() public { 177 | uint AMOUNT_TO_DEPOSIT = 7000000; 178 | dnft.mintNft{value: 5 ether}(address(this)); 179 | // withdraw dyad -> so we have something to deposit 180 | dnft.withdraw(0, AMOUNT_TO_DEPOSIT); 181 | // we need to approve the dnft contract to spend our dyad 182 | dyad.approve(address(dnft), AMOUNT_TO_DEPOSIT); 183 | dnft.deposit (0, AMOUNT_TO_DEPOSIT); 184 | dnft.withdraw(0, 1); 185 | } 186 | function testFailDepositDyadNotNftOwner() public { 187 | dnft.mintNft{value: 5 ether}(address(this)); 188 | vm.prank(address(0)); 189 | dnft.deposit(0, 7000000); 190 | } 191 | function testFailDepositDyadExceedsWithdrawn() public { 192 | // msg.sender needs some dyad to deposit something 193 | dnft.mintNft{value: 5 ether}(address(this)); 194 | dyad.approve(address(dnft), 100); 195 | // msg.sender does not own any dyad withdrawn so we can't deposit 196 | dnft.deposit(0, 100); 197 | } 198 | 199 | // --------------------- DYAD Redeem --------------------- 200 | uint REDEEM_AMOUNT = 100000000; 201 | 202 | function mintAndTransfer(uint amount) public { 203 | // mint -> withdraw -> transfer -> approve dNFT 204 | dnft.mintNft{value: 5 ether}(address(this)); 205 | dnft.withdraw(0, amount); 206 | dyad.approve(address(dnft), amount); 207 | } 208 | function testRedeemDyad() public { 209 | mintAndTransfer(REDEEM_AMOUNT); 210 | 211 | uint totalSupplyBefore = dyad.totalSupply(); 212 | uint withdrawlsBefore = dnft.idToNft(0).withdrawn; 213 | uint dyadBalanceBefore = dyad.balanceOf(address(this)); 214 | 215 | dnft.redeem(0, REDEEM_AMOUNT); 216 | 217 | uint totalSupplyAfter = dyad.totalSupply(); 218 | uint withdrawlsAfter = dnft.idToNft(0).withdrawn; 219 | uint dyadBalanceAfter = dyad.balanceOf(address(this)); 220 | 221 | assertTrue(totalSupplyBefore > totalSupplyAfter); 222 | assertEq(withdrawlsAfter, 0); 223 | assertTrue(withdrawlsBefore > withdrawlsAfter); 224 | assertEq(dyadBalanceAfter, 0); 225 | assertTrue(dyadBalanceBefore > dyadBalanceAfter); 226 | } 227 | function testRedeemDyadSenderDyadBalance() public { 228 | mintAndTransfer(REDEEM_AMOUNT); 229 | uint ethBalanceBefore = address(this).balance; 230 | dnft.redeem(0, REDEEM_AMOUNT); 231 | // before redeeming, the eth balance should be lower than after it 232 | assertTrue(ethBalanceBefore < address(this).balance); 233 | } 234 | function testRedeemDyadPoolBalance() public { 235 | mintAndTransfer(REDEEM_AMOUNT); 236 | uint oldPoolBalance = address(dnft).balance; 237 | dnft.redeem(0, REDEEM_AMOUNT); 238 | // before redeeming, the pool balance should be higher than after it 239 | assertTrue(address(dnft).balance < oldPoolBalance); 240 | } 241 | function testRedeemDyadTotalSupply() public { 242 | mintAndTransfer(REDEEM_AMOUNT); 243 | uint oldDyadTotalSupply = dyad.totalSupply(); 244 | dnft.redeem(0, REDEEM_AMOUNT); 245 | // the redeem burns the dyad so the total supply should be less 246 | assertTrue(dyad.totalSupply() < oldDyadTotalSupply); 247 | } 248 | function testFailRedeemNotNftOwner() public { 249 | // this should fail beacuse msg.sender is not the owner of dnft 1 250 | mintAndTransfer(REDEEM_AMOUNT); 251 | dnft.redeem(1, REDEEM_AMOUNT); 252 | } 253 | 254 | // --------------------- Move Deposit --------------------- 255 | function testMoveDeposit() public { 256 | uint id1 = dnft.mintNft{value: 5 ether}(address(this)); 257 | uint id2 = dnft.mintNft{value: 5 ether}(address(this)); 258 | dnft.moveDeposit(id1, id2, 100); 259 | } 260 | function testFailMoveDepositExceedsDeposit() public { 261 | uint id1 = dnft.mintNft{value: 5 ether}(address(this)); 262 | uint id2 = dnft.mintNft{value: 5 ether}(address(this)); 263 | // without +1 it would succeed 264 | dnft.moveDeposit(id1, id2, ORACLE_PRICE*50000000000+1); 265 | } 266 | function testFailMoveDepositNotNftOwner() public { 267 | uint id1 = dnft.mintNft{value: 5 ether}(address(this)); 268 | uint id2 = dnft.mintNft{value: 5 ether}(address(this)); 269 | vm.prank(address(0)); 270 | dnft.moveDeposit(id1, id2, 100); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /test/dyad.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "forge-std/console.sol"; 6 | import "../src/core/Dyad.sol"; 7 | 8 | contract DYADTest is Test { 9 | DYAD public dyad; 10 | 11 | function setUp() public { 12 | dyad = new DYAD(); 13 | } 14 | 15 | function testMinter() public { 16 | assertEq(dyad.owner(), address(this)); 17 | dyad.transferOwnership(address(1)); 18 | assertEq(dyad.owner(), address(1)); 19 | 20 | // we can't transfer ownership again 21 | vm.expectRevert(); 22 | dyad.transferOwnership(address(1)); 23 | } 24 | 25 | function testMint() public { } 26 | } 27 | -------------------------------------------------------------------------------- /test/interfaces/ICheatCodes.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | interface ICheatCodes { 5 | // Gets address for a given private key, (privateKey) => (address) 6 | function addr(uint256) external returns (address); 7 | } 8 | -------------------------------------------------------------------------------- /test/stake/Staking.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | // import "forge-std/Test.sol"; 5 | // import "forge-std/console.sol"; 6 | // import "ds-test/test.sol"; 7 | 8 | // import {IdNFT} from "../../src/interfaces/IdNFT.sol"; 9 | // import {dNFT} from "../../src/core/dNFT.sol"; 10 | // import {OracleMock} from "./../Oracle.t.sol"; 11 | // import "../../src/core/Dyad.sol"; 12 | // import {Deployment} from "../../script/Deployment.sol"; 13 | // import {Parameters} from "../../script/Parameters.sol"; 14 | // import {Staking, Position} from "../../src/stake/Staking.sol"; 15 | 16 | // uint constant DEPOSIT_MINIMUM = 5000000000000000000000; 17 | // uint constant ORACLE_PRICE = 120000000000; // $1.2k 18 | 19 | // interface CheatCodes { 20 | // function addr(uint256) external returns (address); 21 | // } 22 | 23 | // contract StakeTest is Test, Deployment, Parameters { 24 | // using stdStorage for StdStorage; 25 | 26 | // OracleMock public oracle; 27 | // IdNFT public dnft; 28 | // DYAD public dyad; 29 | // Staking public staking; 30 | // CheatCodes cheats = CheatCodes(HEVM_ADDRESS); 31 | // address public addr1; 32 | 33 | // function setOraclePrice(uint price) public { 34 | // vm.store(address(oracle), bytes32(uint(0)), bytes32(price)); 35 | // } 36 | 37 | // function setUp() public { 38 | // oracle = new OracleMock(); 39 | 40 | // setOraclePrice(ORACLE_PRICE); 41 | 42 | // address _dnft; address _dyad; 43 | // (_dnft, _dyad) = new Deployment().deploy(address(oracle), 44 | // DEPOSIT_MINIMUM, 45 | // BLOCKS_BETWEEN_SYNCS, 46 | // MIN_COLLATERIZATION_RATIO, 47 | // MAX_SUPPLY, 48 | // INSIDERS); 49 | 50 | // dyad = DYAD(_dyad); 51 | // dnft = IdNFT(_dnft); 52 | // staking = new Staking(_dnft, _dyad); 53 | 54 | // addr1 = cheats.addr(1); 55 | // } 56 | 57 | // // function testStake() public { 58 | // // uint amount = 100*10**18; 59 | // // uint id = dnft.mintNft{value: 15 ether}(addr1); 60 | 61 | // // vm.startPrank(addr1); 62 | 63 | // // dyad.approve(address(dnft), amount); 64 | // // dnft.withdraw(id, amount); 65 | // // dnft.approve(address(staking), id); 66 | // // Position memory _position = Position(addr1, 100, addr1, 200, 8000 * 10**18); 67 | // // staking.stake(id, _position); // fee of 1% 68 | // // dyad.approve(address(staking), amount); 69 | // // staking.redeem(id, amount - 200); 70 | // // staking.unstake(id); 71 | 72 | // // dnft.approve(address(staking), id); 73 | // // staking.stake(id, _position); // fee of 1% 74 | 75 | // // vm.stopPrank(); 76 | 77 | // // uint balancePre = dyad.balanceOf(address(this)); 78 | // // staking.mint{value: 5 ether}(id); 79 | // // assertTrue(dyad.balanceOf(address(this)) > balancePre); 80 | // // } 81 | // } 82 | -------------------------------------------------------------------------------- /test/util/Util.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import {OracleMock} from "./../Oracle.t.sol"; 6 | 7 | contract Util is Test { 8 | 9 | function setOraclePrice(OracleMock oracle, uint price) public { 10 | vm.store(address(oracle), bytes32(uint(0)), bytes32(price)); 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /util/addresses.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | P = "../broadcast/Deploy.Goerli.s.sol/5/run-latest.json" 4 | 5 | f = open(P) 6 | d = json.load(f) 7 | 8 | # there are some dups that we need to filter out 9 | contractNames = [] 10 | for k in d["transactions"]: 11 | contractName = k["contractName"] 12 | if contractName not in contractNames: 13 | print(contractName.ljust(12), k["contractAddress"]) 14 | contractNames.append(contractName) 15 | 16 | -------------------------------------------------------------------------------- /util/gas.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | eth_price = 1270 4 | gas_price = 13.7 5 | gas = 853482 6 | calls_per_hour = 6 7 | 8 | @click.command() 9 | @click.option('--gas', default=gas, help='Gas used') 10 | @click.option('--gas_price', default=gas_price, help='Gas price per gwei') 11 | @click.option('--eth_price', default=eth_price, help='ETH price') 12 | @click.option('--calls_per_hour', default=calls_per_hour, help='Calls per hour') 13 | def calc(gas, gas_price, eth_price, calls_per_hour): 14 | print(f"ETH price ${eth_price}") 15 | print(f"Gas price (in gwei) {gas_price}") 16 | print(f"Gas used {gas}") 17 | print(f"Calls per Hour {calls_per_hour}") 18 | print() 19 | print(f"Call Costs ${gas_price*gas/1000000000*eth_price:.2f}") 20 | print(f"Call Costs (per hour) ${calls_per_hour*gas_price*gas/1000000000*eth_price:.2f}") 21 | print(f"Call Costs (per day) ${calls_per_hour*24*gas_price*gas/1000000000*eth_price:.2f}") 22 | 23 | if __name__ == '__main__': 24 | calc() 25 | -------------------------------------------------------------------------------- /util/gas_deployment.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | P = "./broadcast/Deploy.Mainnet.s.sol/1337/run-latest.json" 4 | 5 | f = open(P) 6 | d = json.load(f) 7 | 8 | gas = 0 9 | for k in d["transactions"]: 10 | gas += int(k["transaction"]["gas"], 16) 11 | 12 | print(gas) 13 | --------------------------------------------------------------------------------