diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml new file mode 100644 index 00000000..ae566f7b --- /dev/null +++ b/.github/workflows/unit-tests.yaml @@ -0,0 +1,21 @@ +on: [pull_request] + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - name: Checkout repository and submodules + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Run install + uses: borales/actions-yarn@v4 + with: + cmd: install # will run `yarn install` command + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Run unit tests + run: forge test -vvv \ No newline at end of file diff --git a/addresses/bsc.json b/addresses/bsc.json new file mode 100644 index 00000000..1874e126 --- /dev/null +++ b/addresses/bsc.json @@ -0,0 +1,4 @@ +{ + "interaction": "0xB68443Ee3e828baD1526b3e0Bdf2Dfc6b1975ec4", + "auctionProxy": "0x272d6589CEcC19165cfCd0466f73A648cb1Ea700" +} diff --git a/addresses/bsc_testnet.json b/addresses/bsc_testnet.json new file mode 100644 index 00000000..26b9b078 --- /dev/null +++ b/addresses/bsc_testnet.json @@ -0,0 +1,4 @@ +{ + "interaction": "0x70C4880A3f022b32810a4E9B9F26218Ec026f279", + "auctionProxy": "0xC6e80f443c56F93b2ec1b6bc8942e161dDf22716" +} diff --git a/addresses/new_collateral_SolvBTC.BBN_bsc.json b/addresses/new_collateral_SolvBTC.BBN_bsc.json new file mode 100644 index 00000000..57c64c79 --- /dev/null +++ b/addresses/new_collateral_SolvBTC.BBN_bsc.json @@ -0,0 +1,16 @@ +{ + "symbol": "SolvBTC.BBN", + "tokenAddress": "0x1346b618dC92810EC74163e4c27004c921D446a5", + "ilk": "0x536f6c764254432e42424e000000000000000000000000000000000000000000", + "gemJoin": "0x03DB750d6212C6a0BCa9258E8cB7cf46dfD63067", + "gemJoinImplementation": "0x3e75d7edacc97645033eF8073D025069B0a0976D", + "clipper": "0xEB995ff652da728E7B0EBC31Ab543c39e054b1eA", + "clipperImplementation": "0x9B878823cF06FAC1EdB02B44eADa8bB4274AB7EA", + "oracle": "0x0AD764098FF68b100d0976a8BCF2294B67669cAa", + "oracleImplementation": "0xb35d0F744dDCD92763d37AB3c58716183a99055D", + "oracleInitializeArgs": [ + "0xf3afD82A4071f272F403dC176916141f44E6c750" + ], + "owner": "0xAca0ed4651ddA1F43f00363643CFa5EBF8774b37", + "proxyAdminOwner": "0x08aE09467ff962aF105c23775B9Bc8EAa175D27F" +} diff --git a/addresses/new_collateral_SolvBTC.BBN_bsc_testnet.json b/addresses/new_collateral_SolvBTC.BBN_bsc_testnet.json new file mode 100644 index 00000000..60482f67 --- /dev/null +++ b/addresses/new_collateral_SolvBTC.BBN_bsc_testnet.json @@ -0,0 +1,16 @@ +{ + "symbol": "SolvBTC.BBN", + "tokenAddress": "0x16D9A837e0D1AAC45937425caC26CcB729388C9A", + "ilk": "0x536f6c764254432e42424e000000000000000000000000000000000000000000", + "gemJoin": "0x5adABE1b1fDDFb76c3B7f3Eef9F5DDA7E4f5A347", + "gemJoinImplementation": "0x2c124E030D956F3351A2D205e757941326e3604E", + "clipper": "0xD6A5497e7dbc30a8e9f0b20686a0336B9F2fAc92", + "clipperImplementation": "0x5BA7D1c3f967c00179CE43283C06bcb374838A1D", + "oracle": "0x3e6c4Efe6D6A470439795756BEDE9f4cd6BdDd5d", + "oracleImplementation": "0x5e39f70038Db1083756ED494cf3eADfA07E49ED4", + "oracleInitializeArgs": [ + "0x79e9675cDe605Ef9965AbCE185C5FD08d0DE16B1" + ], + "owner": "0x05E3A7a66945ca9aF73f66660f22ffB36332FA54", + "proxyAdminOwner": "0x05E3A7a66945ca9aF73f66660f22ffB36332FA54" +} diff --git a/audits/README.md b/audits/README.md index c0543301..9b5adf78 100644 --- a/audits/README.md +++ b/audits/README.md @@ -1,18 +1,20 @@ -# Helio Audits +# Lista DAO Audits -This directory the reports of audits performed on Helio smart contracts by different security firms. +This directory the reports of audits performed on Lista DAO smart contracts by different security firms. | :warning: | Audits are not a guarantee of correctness. Some of the contracts were modified after they were audited. | | --------- | :------------------------------------------------------------------------------------------------------ | -| Scope | Firm | Report | -|----------------|------------|------------------------------------------------| -| Ceros, Helio | Certik | [`2022-05-30`](./Certik_300522.pdf) | -| Ceros, Helio | PeckShield | [`2022-05-25`](./PeckShield_250522.pdf) | -| Helio | SlowMist | [`2022-05-10`](./SlowMist_100522.pdf) | -| Ceros | SlowMist | [`2022-05-24`](./SlowMist_240522.pdf) | -| Ceros, Helio | Veridise | [`2022-06-27`](./Veridise_270622.pdf) | -| slisBNB Oracle | PeckShield | [`2024-04-18`](./PeckShield_slisBNBOracle.pdf) | -| Multi-Oracles | PeckShield | [`2024-04-30`](./PeckShield_300424.pdf) | -| OFT | PeckShield | [`2024-06-19`](./PeckShield_190624.pdf) | -| OFT | BlockSec | [`2024-06-19`](./BlockSec_190624.pef) | +| Scope | Firm | Report | +|---------------------------------------|------------|-------------------------------------------------------| +| Ceros, Helio | Certik | [`2022-05-30`](./Certik_300522.pdf) | +| Ceros, Helio | PeckShield | [`2022-05-25`](./PeckShield_250522.pdf) | +| Helio | SlowMist | [`2022-05-10`](./SlowMist_100522.pdf) | +| Ceros | SlowMist | [`2022-05-24`](./SlowMist_240522.pdf) | +| Ceros, Helio | Veridise | [`2022-06-27`](./Veridise_270622.pdf) | +| slisBNB Oracle | PeckShield | [`2024-04-18`](./PeckShield_slisBNBOracle.pdf) | +| Multi-Oracles | PeckShield | [`2024-04-30`](./PeckShield_300424.pdf) | +| OFT | PeckShield | [`2024-06-19`](./PeckShield_190624.pdf) | +| OFT | BlockSec | [`2024-06-19`](./BlockSec_190624.pdf) | +| Emission Voting & clicBNB Launch Pool | BlockSec | [`2024-10-21`](./blocksec_emission_voting_211024.pdf) | +| Emission Voting & clicBNB Launch Pool | Salus | [`2024-10-21`](./salus_emission_voting_211024.pdf) | diff --git a/audits/blocksec_emission_voting_211024.pdf b/audits/blocksec_emission_voting_211024.pdf new file mode 100644 index 00000000..12368e1b Binary files /dev/null and b/audits/blocksec_emission_voting_211024.pdf differ diff --git a/audits/salus_emission_voting_211024.pdf b/audits/salus_emission_voting_211024.pdf new file mode 100644 index 00000000..a7f3e40e Binary files /dev/null and b/audits/salus_emission_voting_211024.pdf differ diff --git a/contracts/Interaction.sol b/contracts/Interaction.sol index 4e638862..b7eb1bb5 100644 --- a/contracts/Interaction.sol +++ b/contracts/Interaction.sol @@ -151,25 +151,6 @@ contract Interaction is OwnableUpgradeable, IDao, IAuctionProxy { hay.safeApprove(hayJoin_, type(uint256).max); } - function setCores(address vat_, address spot_, address hayJoin_, - address jug_) public auth { - // Reset previous approval - hay.safeApprove(address(hayJoin), 0); - - vat = VatLike(vat_); - spotter = SpotLike(spot_); - hayJoin = HayJoinLike(hayJoin_); - jug = JugLike(jug_); - - vat.hope(hayJoin_); - - hay.safeApprove(hayJoin_, type(uint256).max); - } - - function setHayApprove() public auth { - hay.safeApprove(address(hayJoin), type(uint256).max); - } - function setCollateralType( address token, address gemJoin, @@ -211,17 +192,6 @@ contract Interaction is OwnableUpgradeable, IDao, IAuctionProxy { emit CollateralDisabled(token, collaterals[token].ilk); } - function stringToBytes32(string memory source) public pure returns (bytes32 result) { - bytes memory tempEmptyStringTest = bytes(source); - if (tempEmptyStringTest.length == 0) { - return 0x0; - } - - assembly { - result := mload(add(source, 32)) - } - } - function deposit( address participant, address token, @@ -275,7 +245,7 @@ contract Interaction is OwnableUpgradeable, IDao, IAuctionProxy { (uint256 ink, uint256 art) = vat.urns(collateralType.ilk, msg.sender); uint256 liqPrice = liquidationPriceForDebt(collateralType.ilk, ink, art); - takeSnapshot(token, msg.sender, art); + takeSnapshot(token, msg.sender, FullMath.mulDiv(art, rate, RAY)); emit Borrow(msg.sender, token, ink, hayAmount, liqPrice); return uint256(dart); @@ -314,7 +284,7 @@ contract Interaction is OwnableUpgradeable, IDao, IAuctionProxy { (uint256 ink, uint256 userDebt) = vat.urns(collateralType.ilk, msg.sender); uint256 liqPrice = liquidationPriceForDebt(collateralType.ilk, ink, userDebt); - takeSnapshot(token, msg.sender, userDebt); + takeSnapshot(token, msg.sender, FullMath.mulDiv(userDebt, rate, RAY)); emit Payback(msg.sender, token, realAmount, userDebt, liqPrice); return dart; @@ -344,7 +314,10 @@ contract Interaction is OwnableUpgradeable, IDao, IAuctionProxy { (, uint256 userDebt) = vat.urns(collaterals[token].ilk, user); // sync user debt only if it is greater than 0 if (userDebt > 0) { - takeSnapshot(token, user, userDebt); + CollateralType memory collateralType = collaterals[token]; + _checkIsLive(collateralType.live); + (, uint256 rate,,,) = vat.ilks(collateralType.ilk); + takeSnapshot(token, user, FullMath.mulDiv(userDebt, rate, RAY)); } } @@ -612,10 +585,6 @@ contract Interaction is OwnableUpgradeable, IDao, IAuctionProxy { return ClipperLike(collaterals[token].clip).getStatus(auctionId); } - function upchostClipper(address token) external { - ClipperLike(collaterals[token].clip).upchost(); - } - function getAllActiveAuctionsForToken(address token) external view returns (Sale[] memory sales) { return AuctionProxy.getAllActiveAuctionsForClip(ClipperLike(collaterals[token].clip)); } @@ -624,10 +593,6 @@ contract Interaction is OwnableUpgradeable, IDao, IAuctionProxy { AuctionProxy.resetAuction(auctionId, keeper, hay, hayJoin, vat, collaterals[token]); } - function totalPegLiquidity() external view returns (uint256) { - return IERC20Upgradeable(hay).totalSupply(); - } - function _checkIsLive(uint256 live) internal pure { require(live != 0, "Interaction/inactive collateral"); } diff --git a/contracts/old/InteractionV3.sol b/contracts/old/InteractionV3.sol new file mode 100644 index 00000000..0b9cd82e --- /dev/null +++ b/contracts/old/InteractionV3.sol @@ -0,0 +1,654 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import "../hMath.sol"; +import "../oracle/libraries/FullMath.sol"; +import "../interfaces/VatLike.sol"; +import "../interfaces/HayJoinLike.sol"; +import "../interfaces/GemJoinLike.sol"; +import "../interfaces/JugLike.sol"; +import "../interfaces/DogLike.sol"; +import "../interfaces/PipLike.sol"; +import "../interfaces/SpotLike.sol"; +import "../interfaces/IRewards.sol"; +import "../interfaces/IAuctionProxy.sol"; +import "../interfaces/IBorrowLisUSDListaDistributor.sol"; +import "../interfaces/IDynamicDutyCalculator.sol"; +import "../ceros/interfaces/IHelioProvider.sol"; +import "../ceros/interfaces/IDao.sol"; + +import "../libraries/AuctionProxy.sol"; + +uint256 constant WAD = 10 ** 18; +uint256 constant RAD = 10 ** 45; +uint256 constant YEAR = 31556952; //seconds in year (365.2425 * 24 * 3600) + +contract InteractionV3 is OwnableUpgradeable, IDao, IAuctionProxy { + + mapping(address => uint) public wards; + function rely(address usr) external auth {wards[usr] = 1;} + function deny(address usr) external auth {wards[usr] = 0;} + modifier auth { + require(wards[msg.sender] == 1, "Interaction/not-authorized"); + _; + } + + VatLike public vat; + SpotLike public spotter; + IERC20Upgradeable public hay; + HayJoinLike public hayJoin; + JugLike public jug; + address public dog; + IRewards public helioRewards; // Deprecated + + mapping(address => uint256) public deposits; + mapping(address => CollateralType) public collaterals; + + using SafeERC20Upgradeable for IERC20Upgradeable; + using EnumerableSet for EnumerableSet.AddressSet; + + mapping(address => address) public helioProviders; // e.g. Auction purchase from ceabnbc to abnbc + + uint256 public whitelistMode; + address public whitelistOperator; + mapping(address => uint) public whitelist; + mapping(address => uint) public tokensBlacklist; + bool private _entered; + IDynamicDutyCalculator public dutyCalculator; + uint256 public auctionWhitelistMode; + + mapping(address => uint) public auctionWhitelist; + + IBorrowLisUSDListaDistributor public borrowLisUSDListaDistributor; + + function enableWhitelist() external auth {whitelistMode = 1;} + function disableWhitelist() external auth {whitelistMode = 0;} + function enableAuctionWhitelist() external auth {auctionWhitelistMode = 1;} + function disableAuctionWhitelist() external auth {auctionWhitelistMode = 0;} + + function setWhitelistOperator(address usr) external auth { + whitelistOperator = usr; + } + function addToWhitelist(address[] memory usrs) external operatorOrWard { + for(uint256 i = 0; i < usrs.length; i++) + whitelist[usrs[i]] = 1; + } + function removeFromWhitelist(address[] memory usrs) external operatorOrWard { + for(uint256 i = 0; i < usrs.length; i++) + whitelist[usrs[i]] = 0; + } + function addToAuctionWhitelist(address[] memory usrs) external operatorOrWard { + for(uint256 i = 0; i < usrs.length; i++) + auctionWhitelist[usrs[i]] = 1; + } + function removeFromAuctionWhitelist(address[] memory usrs) external operatorOrWard { + for(uint256 i = 0; i < usrs.length; i++) + auctionWhitelist[usrs[i]] = 0; + } + function addToBlacklist(address[] memory tokens) external auth { + for(uint256 i = 0; i < tokens.length; i++) + tokensBlacklist[tokens[i]] = 1; + } + function removeFromBlacklist(address[] memory tokens) external auth { + for(uint256 i = 0; i < tokens.length; i++) + tokensBlacklist[tokens[i]] = 0; + } + function setListaDistributor(address distributor) external auth { + require(distributor != address(0), "Interaction/lista-distributor-zero-address"); + require(address(borrowLisUSDListaDistributor) != distributor, "Interaction/same-distributor-address"); + borrowLisUSDListaDistributor = IBorrowLisUSDListaDistributor(distributor); + } + modifier whitelisted(address participant) { + if (whitelistMode == 1) + require(whitelist[participant] == 1, "Interaction/not-in-whitelist"); + _; + } + modifier auctionWhitelisted { + if (auctionWhitelistMode == 1) + require(auctionWhitelist[msg.sender] == 1, "Interaction/not-in-auction-whitelist"); + _; + } + modifier operatorOrWard { + require(msg.sender == whitelistOperator || wards[msg.sender] == 1, "Interaction/not-operator-or-ward"); + _; + } + modifier notInBlacklisted(address token) { + require (tokensBlacklist[token] == 0, "Interaction/token-in-blacklist"); + _; + } + modifier nonReentrant { + require(!_entered, "re-entrant call"); + _entered = true; + _; + _entered = false; + } + function initialize( + address vat_, + address spot_, + address hay_, + address hayJoin_, + address jug_, + address dog_, + address rewards_ + ) public initializer { + __Ownable_init(); + + wards[msg.sender] = 1; + + vat = VatLike(vat_); + spotter = SpotLike(spot_); + hay = IERC20Upgradeable(hay_); + hayJoin = HayJoinLike(hayJoin_); + jug = JugLike(jug_); + dog = dog_; + helioRewards = IRewards(rewards_); + + vat.hope(hayJoin_); + + hay.safeApprove(hayJoin_, type(uint256).max); + } + + function setCores(address vat_, address spot_, address hayJoin_, + address jug_) public auth { + // Reset previous approval + hay.safeApprove(address(hayJoin), 0); + + vat = VatLike(vat_); + spotter = SpotLike(spot_); + hayJoin = HayJoinLike(hayJoin_); + jug = JugLike(jug_); + + vat.hope(hayJoin_); + + hay.safeApprove(hayJoin_, type(uint256).max); + } + + function setHayApprove() public auth { + hay.safeApprove(address(hayJoin), type(uint256).max); + } + + function setCollateralType( + address token, + address gemJoin, + bytes32 ilk, + address clip, + uint256 mat + ) external auth { + require(collaterals[token].live == 0, "Interaction/token-already-init"); + vat.init(ilk); + jug.init(ilk); + spotter.file(ilk, "mat", mat); + collaterals[token] = CollateralType(GemJoinLike(gemJoin), ilk, 1, clip); + IERC20Upgradeable(token).safeApprove(gemJoin, type(uint256).max); + vat.rely(gemJoin); + emit CollateralEnabled(token, ilk); + } + + function setCollateralDuty(address token, uint256 duty) public auth { + _setCollateralDuty(token, duty); + } + + function _setCollateralDuty(address token, uint256 duty) private { + CollateralType memory collateralType = collaterals[token]; + _checkIsLive(collateralType.live); + jug.drip(collateralType.ilk); + jug.file(collateralType.ilk, "duty", duty); + } + + function setHelioProvider(address token, address helioProvider) external auth { + helioProviders[token] = helioProvider; + } + + function removeCollateralType(address token) external auth { + require(collaterals[token].live != 0, "Interaction/token-not-init"); + collaterals[token].live = 2; //STOPPED + address gemJoin = address(collaterals[token].gem); + vat.deny(gemJoin); + IERC20Upgradeable(token).safeApprove(gemJoin, 0); + emit CollateralDisabled(token, collaterals[token].ilk); + } + + function stringToBytes32(string memory source) public pure returns (bytes32 result) { + bytes memory tempEmptyStringTest = bytes(source); + if (tempEmptyStringTest.length == 0) { + return 0x0; + } + + assembly { + result := mload(add(source, 32)) + } + } + + function deposit( + address participant, + address token, + uint256 dink + ) external whitelisted(participant) notInBlacklisted(token) nonReentrant returns (uint256) { + CollateralType memory collateralType = collaterals[token]; + require(collateralType.live == 1, "Interaction/inactive-collateral"); + + if (helioProviders[token] != address(0)) { + require( + msg.sender == helioProviders[token], + "Interaction/only helio provider can deposit for this token" + ); + } + require(dink <= uint256(type(int256).max), "Interaction/too-much-requested"); + drip(token); + uint256 preBalance = IERC20Upgradeable(token).balanceOf(address(this)); + IERC20Upgradeable(token).safeTransferFrom(msg.sender, address(this), dink); + uint256 postBalance = IERC20Upgradeable(token).balanceOf(address(this)); + require(preBalance + dink == postBalance, "Interaction/deposit-deflated"); + + collateralType.gem.join(participant, dink); + vat.behalf(participant, address(this)); + vat.frob(collateralType.ilk, participant, participant, participant, int256(dink), 0); + + deposits[token] += dink; + + emit Deposit(participant, token, dink, locked(token, participant)); + return dink; + } + + function borrow(address token, uint256 hayAmount) external notInBlacklisted(token) nonReentrant returns (uint256) { + CollateralType memory collateralType = collaterals[token]; + require(collateralType.live == 1, "Interaction/inactive-collateral"); + + drip(token); + poke(token); + + (, uint256 rate, , ,) = vat.ilks(collateralType.ilk); + int256 dart = int256(hayAmount * RAY / rate); + require(dart >= 0, "Interaction/too-much-requested"); + + if (uint256(dart) * rate < hayAmount * RAY) { + dart += 1; //ceiling + } + + vat.frob(collateralType.ilk, msg.sender, msg.sender, msg.sender, 0, dart); + vat.move(msg.sender, address(this), hayAmount * RAY); + hayJoin.exit(msg.sender, hayAmount); + + (uint256 ink, uint256 art) = vat.urns(collateralType.ilk, msg.sender); + uint256 liqPrice = liquidationPriceForDebt(collateralType.ilk, ink, art); + + takeSnapshot(token, msg.sender, art); + + emit Borrow(msg.sender, token, ink, hayAmount, liqPrice); + return uint256(dart); + } + + // Burn user's HAY. + // N.B. User collateral stays the same. + function payback(address token, uint256 hayAmount) external nonReentrant returns (int256) { + CollateralType memory collateralType = collaterals[token]; + // _checkIsLive(collateralType.live); Checking in the `drip` function + + drip(token); + poke(token); + (,uint256 rate,,,) = vat.ilks(collateralType.ilk); + (,uint256 art) = vat.urns(collateralType.ilk, msg.sender); + + int256 dart; + uint256 realAmount = hayAmount; + + uint256 debt = rate * art; + if (realAmount * RAY >= debt) { // Close CDP + dart = int(art); + realAmount = debt / RAY; + realAmount = realAmount * RAY == debt ? realAmount : realAmount + 1; + } else { // Less/Greater than dust + dart = int256(FullMath.mulDiv(realAmount, RAY, rate)); + } + + IERC20Upgradeable(hay).safeTransferFrom(msg.sender, address(this), realAmount); + hayJoin.join(msg.sender, realAmount); + + require(dart >= 0, "Interaction/too-much-requested"); + + vat.frob(collateralType.ilk, msg.sender, msg.sender, msg.sender, 0, - dart); + + (uint256 ink, uint256 userDebt) = vat.urns(collateralType.ilk, msg.sender); + uint256 liqPrice = liquidationPriceForDebt(collateralType.ilk, ink, userDebt); + + takeSnapshot(token, msg.sender, userDebt); + + emit Payback(msg.sender, token, realAmount, userDebt, liqPrice); + return dart; + } + + /** + * @dev take snapshot of user's debt + * @param token collateral token address + * @param user user address + */ + function takeSnapshot(address token, address user, uint256 amount) private { + // ensure the distributor address is set + if (address(borrowLisUSDListaDistributor) != address(0)) { + borrowLisUSDListaDistributor.takeSnapshot(token, user, amount); + } + } + + /** + * @dev synchronize user's debt to the snapshot contract + * @notice this function can be called by anyone + it also act as an initialisation function of user's snapshot data + * @param token collateral token address + * @param user user address + */ + function syncSnapshot(address token, address user) external { + // check user debt is 0? + (, uint256 userDebt) = vat.urns(collaterals[token].ilk, user); + // sync user debt only if it is greater than 0 + if (userDebt > 0) { + takeSnapshot(token, user, userDebt); + } + } + + // Unlock and transfer to the user `dink` amount of ceABNBc + function withdraw( + address participant, + address token, + uint256 dink + ) external nonReentrant returns (uint256) { + CollateralType memory collateralType = collaterals[token]; + _checkIsLive(collateralType.live); + + drip(token); + poke(token); + if (helioProviders[token] != address(0)) { + require( + msg.sender == helioProviders[token], + "Interaction/Only helio provider can call this function for this token" + ); + } else { + require( + msg.sender == participant, + "Interaction/Caller must be the same address as participant" + ); + } + + uint256 unlocked = free(token, participant); + if (unlocked < dink) { + int256 diff = int256(dink) - int256(unlocked); + vat.frob(collateralType.ilk, participant, participant, participant, - diff, 0); + } + // move the dink amount of collateral from participant to the current contract + vat.flux(collateralType.ilk, participant, address(this), dink); + // Collateral is actually transferred back to user inside `exit` operation. + // See GemJoin.exit() + collateralType.gem.exit(msg.sender, dink); + deposits[token] -= dink; + + emit Withdraw(participant, dink); + return dink; + } + + function drip(address token) public { + CollateralType memory collateralType = collaterals[token]; + _checkIsLive(collateralType.live); + + bytes32 _ilk = collateralType.ilk; + (uint256 currentDuty,) = jug.ilks(_ilk); + uint256 duty = dutyCalculator.calculateDuty(token, currentDuty, true); + if (duty != currentDuty) { + _setCollateralDuty(token, duty); + } else { + jug.drip(_ilk); + } + } + + function poke(address token) public { + CollateralType memory collateralType = collaterals[token]; + _checkIsLive(collateralType.live); + + spotter.poke(collateralType.ilk); + } + + // ///////////////////////////////// + // //// VIEW //// + // ///////////////////////////////// + + // Price of the collateral asset(ceABNBc) from Oracle + function collateralPrice(address token) public view returns (uint256) { + CollateralType memory collateralType = collaterals[token]; + _checkIsLive(collateralType.live); + + (PipLike pip,) = spotter.ilks(collateralType.ilk); + (bytes32 price, bool has) = pip.peek(); + require(has, "Interaction/invalid-price"); + return uint256(price); + } + + // Returns the HAY price in $ + function hayPrice(address token) external view returns (uint256) { + CollateralType memory collateralType = collaterals[token]; + _checkIsLive(collateralType.live); + + (, uint256 rate,,,) = vat.ilks(collateralType.ilk); + return rate / 10 ** 9; + } + + // Returns the collateral ratio in percents with 18 decimals + function collateralRate(address token) external view returns (uint256) { + CollateralType memory collateralType = collaterals[token]; + _checkIsLive(collateralType.live); + + (,uint256 mat) = spotter.ilks(collateralType.ilk); + require(mat != 0, "Interaction/spot-not-init"); + return 10 ** 45 / mat; + } + + // Total ceABNBc deposited nominated in $ + function depositTVL(address token) external view returns (uint256) { + return deposits[token] * collateralPrice(token) / WAD; + } + + // Total HAY borrowed by all users + function collateralTVL(address token) external view returns (uint256) { + CollateralType memory collateralType = collaterals[token]; + _checkIsLive(collateralType.live); + + (uint256 Art, uint256 rate,,,) = vat.ilks(collateralType.ilk); + return FullMath.mulDiv(Art, rate, RAY); + } + + // Not locked user balance in ceABNBc + function free(address token, address usr) public view returns (uint256) { + CollateralType memory collateralType = collaterals[token]; + _checkIsLive(collateralType.live); + + return vat.gem(collateralType.ilk, usr); + } + + // User collateral in ceABNBc + function locked(address token, address usr) public view returns (uint256) { + CollateralType memory collateralType = collaterals[token]; + _checkIsLive(collateralType.live); + + (uint256 ink,) = vat.urns(collateralType.ilk, usr); + return ink; + } + + // Total borrowed HAY + function borrowed(address token, address usr) external view returns (uint256) { + CollateralType memory collateralType = collaterals[token]; + _checkIsLive(collateralType.live); + + (,uint256 rate,,,) = vat.ilks(collateralType.ilk); + (, uint256 art) = vat.urns(collateralType.ilk, usr); + + // 100 Wei is added as a ceiling to help close CDP in repay() + if ((art * rate) / RAY != 0) { + return ((art * rate) / RAY) + 100; + } + else { + return 0; + } + } + + // Collateral minus borrowed. Basically free collateral (nominated in HAY) + function availableToBorrow(address token, address usr) external view returns (int256 amount) { + CollateralType memory collateralType = collaterals[token]; + _checkIsLive(collateralType.live); + + (uint256 ink, uint256 art) = vat.urns(collateralType.ilk, usr); + (, uint256 rate, uint256 spot,,) = vat.ilks(collateralType.ilk); + uint256 collateral = ink * spot; + uint256 debt = rate * art; + amount = (int256(collateral) - int256(debt)) / 1e27; + + if(amount < 0) return 0; + } + + // Collateral + `amount` minus borrowed. Basically free collateral (nominated in HAY) + // Returns how much hay you can borrow if provide additional `amount` of collateral + function willBorrow(address token, address usr, int256 amount) external view returns (int256) { + CollateralType memory collateralType = collaterals[token]; + _checkIsLive(collateralType.live); + + (uint256 ink, uint256 art) = vat.urns(collateralType.ilk, usr); + (, uint256 rate, uint256 spot,,) = vat.ilks(collateralType.ilk); + require(amount >= - (int256(ink)), "Cannot withdraw more than current amount"); + if (amount < 0) { + ink = uint256(int256(ink) + amount); + } else { + ink += uint256(amount); + } + uint256 collateral = ink * spot; + uint256 debt = rate * art; + return (int256(collateral) - int256(debt)) / 1e27; + } + + function liquidationPriceForDebt(bytes32 ilk, uint256 ink, uint256 art) internal view returns (uint256) { + if (ink == 0) { + return 0; // no meaningful price if user has no debt + } + (, uint256 rate,,,) = vat.ilks(ilk); + (,uint256 mat) = spotter.ilks(ilk); + uint256 backedDebt = (art * rate / 10 ** 36) * mat; + return backedDebt / ink; + } + + // Price of ceABNBc when user will be liquidated + function currentLiquidationPrice(address token, address usr) external view returns (uint256) { + CollateralType memory collateralType = collaterals[token]; + _checkIsLive(collateralType.live); + + (uint256 ink, uint256 art) = vat.urns(collateralType.ilk, usr); + return liquidationPriceForDebt(collateralType.ilk, ink, art); + } + + // Returns borrow APR with 20 decimals. + // I.e. 10% == 10 ethers + function borrowApr(address token) public view returns (uint256) { + CollateralType memory collateralType = collaterals[token]; + _checkIsLive(collateralType.live); + + (uint256 duty,) = jug.ilks(collateralType.ilk); + uint256 principal = hMath.rpow((jug.base() + duty), YEAR, RAY); + return (principal - RAY) / (10 ** 7); + } + + function startAuction( + address token, + address user, + address keeper + ) external auctionWhitelisted returns (uint256) { + + drip(token); + poke(token); + + CollateralType memory collateral = collaterals[token]; + (uint256 ink,) = vat.urns(collateral.ilk, user); + IHelioProvider provider = IHelioProvider(helioProviders[token]); + uint256 auctionAmount = AuctionProxy.startAuction( + user, + keeper, + hay, + hayJoin, + vat, + DogLike(dog), + provider, + collateral + ); + // after auction started, user's debt of the token becomes 0 + takeSnapshot(token, user, 0); + + emit AuctionStarted(token, user, ink, collateralPrice(token)); + return auctionAmount; + } + + function buyFromAuction( + address token, + uint256 auctionId, + uint256 collateralAmount, + uint256 maxPrice, + address receiverAddress + ) external auctionWhitelisted { + CollateralType memory collateral = collaterals[token]; + IHelioProvider helioProvider = IHelioProvider(helioProviders[token]); + uint256 leftover = AuctionProxy.buyFromAuction( + auctionId, + collateralAmount, + maxPrice, + receiverAddress, + hay, + hayJoin, + vat, + helioProvider, + collateral + ); + + address urn = ClipperLike(collateral.clip).sales(auctionId).usr; // Liquidated address + + emit Liquidation(urn, token, collateralAmount, leftover); + } + + function getAuctionStatus(address token, uint256 auctionId) external view returns(bool, uint256, uint256, uint256) { + return ClipperLike(collaterals[token].clip).getStatus(auctionId); + } + + function upchostClipper(address token) external { + ClipperLike(collaterals[token].clip).upchost(); + } + + function getAllActiveAuctionsForToken(address token) external view returns (Sale[] memory sales) { + return AuctionProxy.getAllActiveAuctionsForClip(ClipperLike(collaterals[token].clip)); + } + + function resetAuction(address token, uint256 auctionId, address keeper) external auctionWhitelisted { + AuctionProxy.resetAuction(auctionId, keeper, hay, hayJoin, vat, collaterals[token]); + } + + function totalPegLiquidity() external view returns (uint256) { + return IERC20Upgradeable(hay).totalSupply(); + } + + function _checkIsLive(uint256 live) internal pure { + require(live != 0, "Interaction/inactive collateral"); + } + + function setDutyCalculator(address _dutyCalculator) external auth { + require(_dutyCalculator != address(0) && _dutyCalculator != address(dutyCalculator), "Interaction/invalid-dutyCalculator-address"); + dutyCalculator = IDynamicDutyCalculator(_dutyCalculator); + require(dutyCalculator.interaction() == address(this), "Interaction/invalid-dutyCalculator-interaction"); + } + + /** + * @dev Returns the next duty for the given collateral. This function is used by the frontend to display the next duty. + * Can be accessed as a view from within the UX since no state changes and no events emitted. + * @param _collateral The address of the collateral + * @return duty The next duty + */ + function getNextDuty(address _collateral) external returns (uint256 duty) { + CollateralType memory collateral = collaterals[_collateral]; + + (uint256 currentDuty,) = jug.ilks(collateral.ilk); + + duty = dutyCalculator.calculateDuty(_collateral, currentDuty, false); + } +} diff --git a/contracts/oracle/SolvBTCBBNOracle.sol b/contracts/oracle/SolvBTCBBNOracle.sol new file mode 100644 index 00000000..ab1c2dd1 --- /dev/null +++ b/contracts/oracle/SolvBTCBBNOracle.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "./interfaces/IResilientOracle.sol"; + +contract SolvBTCBBNOracle is Initializable { + + address constant SOLVE_BTC_TOKEN_ADDR = 0x4aae823a6a0b376De6A78e74eCC5b079d38cBCf7; + address constant SOLVE_BTC_BBN_TOKEN_ADDR = 0x1346b618dC92810EC74163e4c27004c921D446a5; + + IResilientOracle public resilientOracle; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(address _resilientOracle) external initializer { + resilientOracle = IResilientOracle(_resilientOracle); + } + + /** + * Returns the latest price + */ + function peek() public view returns (bytes32, bool) { + // get Btc/USD price (8 decimals) + uint256 solveBtcPrice = resilientOracle.peek(SOLVE_BTC_TOKEN_ADDR); + // get SolveBtcBBN/Btc price (8 decimals) + uint256 solveBtcBbnPrice = resilientOracle.peek(SOLVE_BTC_BBN_TOKEN_ADDR); + // calculate SolveBtcBBN/USD (18 decimals) + return (bytes32(uint(solveBtcPrice) * uint(solveBtcBbnPrice) * 1e2), true); + } +} diff --git a/contracts/upgrade/TimeLock.sol b/contracts/upgrade/TimeLock.sol new file mode 100644 index 00000000..b748dd56 --- /dev/null +++ b/contracts/upgrade/TimeLock.sol @@ -0,0 +1,13 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.10; + +import "@openzeppelin/contracts/governance/TimelockController.sol"; + +contract TimeLock is TimelockController { + constructor( + uint256 minDelay, + address[] memory proposers, + address[] memory executors, + address admin + ) TimelockController(minDelay, proposers, executors, admin) {} +} diff --git a/diagrams/Collateral-Borrow-Sequence.drawio.png b/diagrams/Collateral-Borrow-Sequence.drawio.png new file mode 100644 index 00000000..b7f1938f Binary files /dev/null and b/diagrams/Collateral-Borrow-Sequence.drawio.png differ diff --git a/diagrams/Collateral-Deposit-Sequence.drawio.png b/diagrams/Collateral-Deposit-Sequence.drawio.png new file mode 100644 index 00000000..451b147e Binary files /dev/null and b/diagrams/Collateral-Deposit-Sequence.drawio.png differ diff --git a/diagrams/Collateral-Payback-Sequence.drawio.png b/diagrams/Collateral-Payback-Sequence.drawio.png new file mode 100644 index 00000000..0d6cf6a9 Binary files /dev/null and b/diagrams/Collateral-Payback-Sequence.drawio.png differ diff --git a/diagrams/Collateral-Withdraw-Sequence.drawio.png b/diagrams/Collateral-Withdraw-Sequence.drawio.png new file mode 100644 index 00000000..b82062a0 Binary files /dev/null and b/diagrams/Collateral-Withdraw-Sequence.drawio.png differ diff --git a/foundry.toml b/foundry.toml index 77c6e3e0..288c523b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,11 +1,20 @@ [profile.default] src = 'contracts' out = 'out' -libs = ['node_modules', 'lib'] +libs = ['lib'] test = 'test' cache_path = 'cache_forge' solc = "0.8.10" +optimizer_runs = 200 + +remappings=[ + "@openzeppelin/contracts=./node_modules/@openzeppelin/contracts", + "@openzeppelin/contracts-upgradeable=./node_modules/@openzeppelin/contracts-upgradeable", + "hardhat=./node_modules/hardhat", + "@chainlink/contracts=./node_modules/@chainlink/contracts", + "@pythnetwork/pyth-sdk-solidity=./node_modules/@pythnetwork/pyth-sdk-solidity", +] [rpc_endpoints] -bsc-test = "https://data-seed-prebsc-1-s1.binance.org:8545/" -bsc-main = "https://bsc-dataseed.binance.org/" \ No newline at end of file +bsc-test = "https://bsc-testnet.bnbchain.org" +bsc-main = "https://bsc-dataseed.bnbchain.org" diff --git a/scripts/deploy/8_new_collateral_solvebtc_bbn.js b/scripts/deploy/8_new_collateral_solvebtc_bbn.js new file mode 100644 index 00000000..f5d65602 --- /dev/null +++ b/scripts/deploy/8_new_collateral_solvebtc_bbn.js @@ -0,0 +1,60 @@ +const {ethers, upgrades} = require('hardhat') +const {addCollateral} = require('../utils/add_collateral') + +// Global Variables +let rad = '000000000000000000000000000000000000000000000' // 45 Decimals + +async function main() { + + [deployer] = await ethers.getSigners() + let NEW_OWNER = '0xAca0ed4651ddA1F43f00363643CFa5EBF8774b37' + let PROXY_ADMIN_OWNER = '0x08aE09467ff962aF105c23775B9Bc8EAa175D27F' + + const symbol = 'SolvBTC.BBN' + let tokenAddress = '0x1346b618dC92810EC74163e4c27004c921D446a5' // SolvBTC.BBN token address on BSC Mainnet + let oracleName = 'SolvBTCBBNOracle'; + let oracleInitializeArgs = ['0xf3afD82A4071f272F403dC176916141f44E6c750']; + let oracleInitializer = 'initialize'; + + if (hre.network.name === 'bsc_testnet') { + NEW_OWNER = process.env.OWNER || deployer.address + PROXY_ADMIN_OWNER = process.env.PROXY_ADMIN_OWNER || deployer.address + console.log('Deploying on BSC Testnet', hre.network.name, 'Network', deployer.address) + // deploy token + const ERC20UpgradeableMock = await hre.ethers.getContractFactory('ERC20UpgradeableMock') + const tokenMock = await upgrades.deployProxy(ERC20UpgradeableMock, [symbol, symbol]) + await tokenMock.waitForDeployment() + const tokenMockImplementation = await upgrades.erc1967.getImplementationAddress(tokenMock.target) + console.log('Deployed: tokenMock : ' + tokenMock.target) + console.log('Imp: : ' + tokenMockImplementation) + tokenAddress = tokenMock.target + // await hre.run('verify:verify', {address: tokenAddress}) + // mint 10000000 tokens to deployer + await tokenMock.mint(deployer.address, ethers.parseEther('10000000')) + oracleInitializeArgs = ['0x79e9675cDe605Ef9965AbCE185C5FD08d0DE16B1']; + } + + // add collateral + await addCollateral({ + symbol, + tokenAddress, + oracleName, + oracleInitializeArgs, + oracleInitializer, + owner: NEW_OWNER, + proxyAdminOwner: PROXY_ADMIN_OWNER, + clipperBuf: '1100000000000000000000000000', + clipperTail: '10800', + clipperCusp: '600000000000000000000000000', + clipperChip: '0', + clipperTip: '5' + rad, + clipperStopped: '0' + }) +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/deploy/contract_address.json b/scripts/deploy/contract_address.json index 4c9eb617..d628ba9c 100644 --- a/scripts/deploy/contract_address.json +++ b/scripts/deploy/contract_address.json @@ -5,6 +5,8 @@ "SPOT" : "0x49bc2c4E5B035341b7d92Da4e6B267F7426F3038", "INTERACTION" : "0xB68443Ee3e828baD1526b3e0Bdf2Dfc6b1975ec4", "VOW" : "0x2078A1969Ea581D618FDBEa2C0Dc13Fc15CB9fa7", + "// ClassicABACI": "0xBaf8B40a1E2A7B842289778C1c509B6BEdE3Cc03", + "// InnovationABACI": "0xc1359eD77E6B0CBF9a8130a4C28FBbB87B9501b7", "ABACI" : "0xc1359eD77E6B0CBF9a8130a4C28FBbB87B9501b7", "JUG": "0x787BdEaa29A253e40feB35026c3d05C18CbCA7B3" }, diff --git a/scripts/deploy/deploy_interaction_impl.js b/scripts/deploy/deploy_interaction_impl.js new file mode 100644 index 00000000..8a44f6cc --- /dev/null +++ b/scripts/deploy/deploy_interaction_impl.js @@ -0,0 +1,63 @@ +const path = require('path') +const hre = require('hardhat') +const {ethers, upgrades} = require('hardhat') +const {deployImplementation, verifyImpContract} = require('../upgrades/utils/upgrade_utils') + +const filePath = hre.network.name === 'bsc_testnet' ? path.join(process.cwd(), 'addresses/bsc_testnet.json') : path.join(process.cwd(), 'addresses/bsc.json') +let { interaction, auctionProxy } = require(filePath); +const oldContractName = 'InteractionV3' +const contractName = 'Interaction' +const resetAuctionProxy = false + + +async function main() { + console.log('Running deploy script') + + if (resetAuctionProxy) { + console.log('Deploying AuctionProxy...') + const AuctionProxy = await ethers.getContractFactory('AuctionProxy') + const auctionProxyContract = await AuctionProxy.deploy() + await auctionProxyContract.waitForDeployment() + console.log('AuctionProxy deployed to:', auctionProxyContract.address) + auctionProxy = auctionProxyContract.address + } + + const Interaction = await ethers.getContractFactory(contractName, { + unsafeAllow: ['external-library-linking'], + libraries: { + AuctionProxy: auctionProxy, + }, + }) + + console.log('Validate if its upgradable...') + const OldInteraction = await ethers.getContractFactory(oldContractName, { + unsafeAllow: ['external-library-linking'], + libraries: { + AuctionProxy: auctionProxy, + }, + }); + await upgrades.forceImport(interaction, OldInteraction, { kind: 'transparent' }); + await upgrades.validateUpgrade(interaction, Interaction, { unsafeAllow: ['external-library-linking'] }) + console.log('Updatability is validated successfully.') + + + console.log('Deploy Interaction...') + const interactionAddress = await deployImplementation(contractName, { + unsafeAllow: ['external-library-linking'], + libraries: { + AuctionProxy: auctionProxy, + }, + }) + + console.log('Interaction deployed to:', interactionAddress) + + + await verifyImpContract(interactionAddress) +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/dev/amo/setCollateralParams_new.js b/scripts/dev/amo/setCollateralParams_new.js new file mode 100644 index 00000000..0745d748 --- /dev/null +++ b/scripts/dev/amo/setCollateralParams_new.js @@ -0,0 +1,25 @@ +const hre = require("hardhat"); +const {upgrades} = require("hardhat"); + + +async function main() { + let collateral = '0x16D9A837e0D1AAC45937425caC26CcB729388C9A' + let beta = '1000000' + let rate0 = '2293273137447729405' // duty - 1e27 + + const dynamicCalc = '0x1a85d3530840111a662a8E5Ea611aC1089391c6E' + + console.log('DynamicDutyCalculator...') + this.DynamicDutyCalculator = await hre.ethers.getContractFactory('DynamicDutyCalculator') + const dynamicDutyCalculator = this.DynamicDutyCalculator.attach(dynamicCalc) + + const resp = await dynamicDutyCalculator.setCollateralParams(collateral, beta, rate0, 'true') + console.log(resp) +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/dev/new_collaterals/add_collateral_solvbtc_bbn.js b/scripts/dev/new_collaterals/add_collateral_solvbtc_bbn.js new file mode 100644 index 00000000..bdcaf1ea --- /dev/null +++ b/scripts/dev/new_collaterals/add_collateral_solvbtc_bbn.js @@ -0,0 +1,106 @@ +const {ethers, upgrades} = require('hardhat') +const hre = require('hardhat') +// Global Variables +let ray = '000000000000000000000000000', // 27 Decimals + rad = '000000000000000000000000000000000000000000000' // 45 Decimals + +async function main() { + + [deployer] = await ethers.getSigners() + const {symbol, tokenAddress, ilk, gemJoin, clipper, oracle} = { + "symbol": "SolvBTC.BBN", + "tokenAddress": "0x16D9A837e0D1AAC45937425caC26CcB729388C9A", + "ilk": "0x536f6c764254432e42424e000000000000000000000000000000000000000000", + "gemJoin": "0x5adABE1b1fDDFb76c3B7f3Eef9F5DDA7E4f5A347", + "gemJoinImplementation": "0x2c124E030D956F3351A2D205e757941326e3604E", + "clipper": "0xD6A5497e7dbc30a8e9f0b20686a0336B9F2fAc92", + "clipperImplementation": "0x5BA7D1c3f967c00179CE43283C06bcb374838A1D", + "oracle": "0x3e6c4Efe6D6A470439795756BEDE9f4cd6BdDd5d", + "oracleImplementation": "0x5e39f70038Db1083756ED494cf3eADfA07E49ED4", + "oracleInitializeArgs": [ + "0x79e9675cDe605Ef9965AbCE185C5FD08d0DE16B1" + ], + "owner": "0x05E3A7a66945ca9aF73f66660f22ffB36332FA54", + "proxyAdminOwner": "0x05E3A7a66945ca9aF73f66660f22ffB36332FA54" + } + + // core parameters + const mat = '2000000000000000000000000000' // Liquidation Ratio + const line = '500000' + rad // Debt Ceiling + const dust = '15' + rad // Debt Floor + const hole = '5000000' + rad // Liquidation + const chop = '1100000000000000000' // Liquidation + + + let VAT = '0x33A34eAB3ee892D40420507B820347b1cA2201c4' + let DOG = '0xd57E7b53a1572d27A04d9c1De2c4D423f1926d0B' + let SPOT = '0x49bc2c4E5B035341b7d92Da4e6B267F7426F3038' + let INTERACTION = '0xB68443Ee3e828baD1526b3e0Bdf2Dfc6b1975ec4' + let AUCTION_PROXY + + if (hre.network.name === 'bsc_testnet') { + VAT = '0x382589e4dE7A061fcb9716c203983d8FE847AE0b' + DOG = '0x3d2165EDf3Cc07992f54d9310FB800C81BC641F7' + SPOT = '0xa2882B6AC7cBA1b8784BF5D72F38CF0E6416263e' + INTERACTION = '0x70C4880A3f022b32810a4E9B9F26218Ec026f279' + if (!AUCTION_PROXY) { + // deploy AuctionProxy + const AuctionProxy = await hre.ethers.getContractFactory('AuctionProxy') + const auctionProxy = await AuctionProxy.deploy() + await auctionProxy.waitForDeployment() + AUCTION_PROXY = await auctionProxy.getAddress() + console.log('AuctionProxy deployed to:', AUCTION_PROXY) + } + } + + console.log('symbol: ' + symbol) + console.log('tokenAddress: ' + tokenAddress) + console.log('ilk: ' + ilk) + + // configure the collateral + console.log('interaction...') + this.Interaction = await hre.ethers.getContractFactory('Interaction', { + unsafeAllow: ['external-library-linking'], + libraries: { + AuctionProxy: AUCTION_PROXY + }, + }) + const interaction = this.Interaction.attach(INTERACTION) + await interaction.setCollateralType(tokenAddress, gemJoin, ilk, clipper, mat) + + console.log('vat...') + this.Vat = await hre.ethers.getContractFactory('Vat') + const vat = this.Vat.attach(VAT) + await vat.rely(gemJoin) + await vat.rely(clipper) + await vat['file(bytes32,bytes32,uint256)'](ilk, ethers.encodeBytes32String('dust'), dust) + await vat['file(bytes32,bytes32,uint256)'](ilk, ethers.encodeBytes32String('line'), line) + + console.log('spot...') + this.Spot = await hre.ethers.getContractFactory('Spotter') + const spot = this.Spot.attach(SPOT) + await spot['file(bytes32,bytes32,address)'](ilk, ethers.encodeBytes32String('pip'), oracle) + + console.log('Dog...') + this.Dog = await hre.ethers.getContractFactory('Dog') + const dog = this.Dog.attach(DOG) + await dog.rely(clipper) + await dog['file(bytes32,bytes32,uint256)'](ilk, ethers.encodeBytes32String('hole'), hole) + await dog['file(bytes32,bytes32,uint256)'](ilk, ethers.encodeBytes32String('chop'), chop) // 10% + await dog['file(bytes32,bytes32,address)'](ilk, ethers.encodeBytes32String('clip'), clipper) + + + await interaction.setCollateralDuty(tokenAddress, '1000000002293273137447729405'); //apr 7.5% + console.log("set duty..."); + + await interaction.poke(tokenAddress, { gasLimit: 1000000 }) + await interaction.drip(tokenAddress, { gasLimit: 1000000 }) + console.log('Finished') +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/dev/upgrade_interaction.js b/scripts/dev/upgrade_interaction.js index f3b261b2..93a5435e 100644 --- a/scripts/dev/upgrade_interaction.js +++ b/scripts/dev/upgrade_interaction.js @@ -1,42 +1,51 @@ const hre = require("hardhat"); const { ethers, upgrades } = require("hardhat"); -require("@nomiclabs/hardhat-etherscan"); - -const { VAT, - SPOT, - aBNBc, - USB, - UsbJoin, - aBNBcJoin, - Oracle, - JUG, - REAL_ABNBC, - REALaBNBcJoin, - INTERACTION, - AUCTION_PROXY -} = require('../../addresses.json'); + +let { interaction, auctionProxy } = require('../../addresses/bsc_testnet.json'); +const oldContractName = 'InteractionV3' +const contractName = 'Interaction' +const resetAuctionProxy = false async function main() { console.log('Running deploy script'); + if (resetAuctionProxy) { + const AuctionProxy = await hre.ethers.getContractFactory("AuctionProxy"); + const auctionProxyContract = await AuctionProxy.deploy(); + await auctionProxyContract.waitForDeployment(); + console.log("AuctionProxy deployed to: ", auctionProxyContract.address); + auctionProxy = auctionProxyContract.address; + } - const Interaction = await hre.ethers.getContractFactory("Interaction", { + const Interaction = await hre.ethers.getContractFactory(contractName, { unsafeAllow: ['external-library-linking'], libraries: { - AuctionProxy: AUCTION_PROXY + AuctionProxy: auctionProxy }, }); + + console.log('Validate if its upgradable...') + const OldInteraction = await ethers.getContractFactory(oldContractName, { + unsafeAllow: ['external-library-linking'], + libraries: { + AuctionProxy: auctionProxy, + }, + }); + await upgrades.forceImport(interaction, OldInteraction, { kind: 'transparent' }); + await upgrades.validateUpgrade(interaction, Interaction, { unsafeAllow: ['external-library-linking'] }) + console.log('Updatability is validated successfully.') + // // console.log('Force importing proxy'); - // await upgrades.forceImport(INTERACTION, Interaction); + // await upgrades.forceImport(interaction, Interaction); // console.log("Preparing upgrade..."); - // const interactionV2 = await upgrades.prepareUpgrade(INTERACTION, Interaction, { + // const interactionV2 = await upgrades.prepareUpgrade(interaction, Interaction, { // kind: "uups", // unsafeAllowLinkedLibraries: true, // }); // console.log("interactionV2 ", interactionV2); - const upgraded = await upgrades.upgradeProxy(INTERACTION, Interaction, { - kind: "uups", + const upgraded = await upgrades.upgradeProxy(interaction, Interaction, { + kind: "transparent", unsafeAllowLinkedLibraries: true, }); console.log("interactionV2 upgraded with ", upgraded.address); diff --git a/scripts/upgrades/deploy_interaction_impl.js b/scripts/upgrades/deploy_interaction_impl.js deleted file mode 100644 index 785fad1d..00000000 --- a/scripts/upgrades/deploy_interaction_impl.js +++ /dev/null @@ -1,51 +0,0 @@ -const hre = require('hardhat') -const {ethers, upgrades} = require('hardhat') -const {deployImplementation, verifyImpContract} = require('./utils/upgrade_utils') - -const proxyAddress = '0xB68443Ee3e828baD1526b3e0Bdf2Dfc6b1975ec4' -const auctionProxy = '0x272d6589CEcC19165cfCd0466f73A648cb1Ea700' -const contractName = 'Interaction' - - -async function main() { - console.log('Running deploy script') - - const Interaction = await ethers.getContractFactory('Interaction', { - unsafeAllow: ['external-library-linking'], - libraries: { - AuctionProxy: auctionProxy, - }, - }) - - console.log('Validate if its upgradable...') - const OldInteraction = await ethers.getContractFactory('InteractionV2', { - unsafeAllow: ['external-library-linking'], - libraries: { - AuctionProxy: auctionProxy, - }, - }); - await upgrades.forceImport(proxyAddress, OldInteraction, { kind: 'transparent' }); - await upgrades.validateUpgrade(proxyAddress, Interaction, { unsafeAllow: ['external-library-linking'] }) - console.log('Updatability is validated successfully.') - - - console.log('Deploy Interaction...') - const interactionAddress = await deployImplementation(contractName, { - unsafeAllow: ['external-library-linking'], - libraries: { - AuctionProxy: auctionProxy, - }, - }) - - console.log('Interaction deployed to:', interactionAddress) - - - await verifyImpContract(interactionAddress) -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error) - process.exit(1) - }) diff --git a/scripts/upgrades/timelock/deploy_timelock.ts b/scripts/upgrades/timelock/deploy_timelock.ts new file mode 100644 index 00000000..50a8bba9 --- /dev/null +++ b/scripts/upgrades/timelock/deploy_timelock.ts @@ -0,0 +1,33 @@ +import { ethers } from "hardhat"; + +const admin = "0x6616EF47F4d997137a04C2AD7FF8e5c228dA4f06"; +const minDelay = "60"; // 60 seconds +const proposers = [admin]; +const executors = [admin]; + +// Proxy and ProxyAdmin addresses +const proxy = "0x3Cf187f30A64fd4357f4EC8Cc133E5AFFA5dB483"; +const proxyAdmin = "0x867B15a48127a3d766d53B38a8630b93D2Afb791"; +const newImplementation = "0x5BC42792F48039034595949A335D3e7dd2EdeC91"; + +async function main() { + // 1. Deploy TimeLock contract + const TimeLock = await ethers.getContractFactory("TimeLock"); + const timelock = await TimeLock.deploy(minDelay, proposers, executors, admin); + await timelock.waitForDeployment(); + // TimeLock deployed to: 0x54fA4aeca37BC354f79b3E002E111D2844635bfC + console.log("TimeLock deployed to:", timelock.target); + + await run("verify:verify", { + address: timelock.target, + constructorArguments: [minDelay, proposers, executors, admin], + contract: "contracts/upgrade/TimeLock.sol:TimeLock", + }); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/test/foundry/DynamicDutyCalculator.t.sol b/test/foundry/DynamicDutyCalculator.t.sol index 12bd6cca..df418762 100644 --- a/test/foundry/DynamicDutyCalculator.t.sol +++ b/test/foundry/DynamicDutyCalculator.t.sol @@ -103,7 +103,7 @@ contract DynamicDutyCalculatorTest is Test { } function testRevert_setCollateralParams() public { - vm.expectRevert("AccessControl: account 0x34a1d3fff3958843c43ad80f30b94c510645c316 is missing role 0x0000000000000000000000000000000000000000000000000000000000000000"); + vm.expectRevert(); dynamicDutyCalculator.setCollateralParams(collateral, beta, rate0, true); vm.startPrank(admin); @@ -650,7 +650,7 @@ contract DynamicDutyCalculatorTest is Test { } function testRevert_setPriceRange() public { - vm.expectRevert("AccessControl: account 0x34a1d3fff3958843c43ad80f30b94c510645c316 is missing role 0x0000000000000000000000000000000000000000000000000000000000000000"); + vm.expectRevert(); dynamicDutyCalculator.setPriceRange(7e7, 12e7); } @@ -698,7 +698,7 @@ contract DynamicDutyCalculatorTest is Test { } function testRevert_file() public { - vm.expectRevert("AccessControl: account 0x34a1d3fff3958843c43ad80f30b94c510645c316 is missing role 0x0000000000000000000000000000000000000000000000000000000000000000"); + vm.expectRevert(); dynamicDutyCalculator.file("interaction", address(0xAA)); vm.startPrank(admin); diff --git a/test/foundry/TimeLock.t.sol b/test/foundry/TimeLock.t.sol new file mode 100644 index 00000000..97829ce8 --- /dev/null +++ b/test/foundry/TimeLock.t.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.10; + +import "forge-std/Test.sol"; + +import {TimeLock} from "../../contracts/upgrade/TimeLock.sol"; + +contract TimeLockTest is Test { + TimeLock timeLock; + + address proposer = makeAddr("proposer"); + address executor = makeAddr("executor"); + uint256 minDelay = 100; + + function setUp() public { + address[] memory proposers = new address[](1); + proposers[0] = proposer; + address[] memory executors = new address[](1); + executors[0] = executor; + timeLock = new TimeLock(minDelay, proposers, executors, address(0)); // don't set admin + + assertEq(timeLock.hasRole(timeLock.PROPOSER_ROLE(), proposer), true); + assertEq(timeLock.hasRole(timeLock.EXECUTOR_ROLE(), executor), true); + assertEq( + timeLock.hasRole(timeLock.TIMELOCK_ADMIN_ROLE(), address(timeLock)), + true + ); + + // Only TIMELOCK_ADMIN_ROLE can grant roles + assertEq(timeLock.getRoleAdmin(timeLock.PROPOSER_ROLE()), timeLock.TIMELOCK_ADMIN_ROLE()); + assertEq(timeLock.getRoleAdmin(timeLock.EXECUTOR_ROLE()), timeLock.TIMELOCK_ADMIN_ROLE()); + } + + function test_updateDelay() public { + uint256 newDelay = 200; + + bytes memory data = abi.encodeWithSignature( + "updateDelay(uint256)", + newDelay + ); + + address target = address(timeLock); + uint256 value = 0; + bytes32 predecessor = bytes32(0); + bytes32 salt = bytes32(0); + uint256 _delay = 100; + + vm.startPrank(proposer); + timeLock.schedule(target, value, data, predecessor, salt, _delay); + vm.stopPrank(); + + bytes32 id = timeLock.hashOperation( + target, + value, + data, + predecessor, + salt + ); + + assertEq(timeLock.isOperationPending(id), true); // operation is pending + + skip(100); + + vm.startPrank(executor); + timeLock.execute(target, value, data, predecessor, salt); + vm.stopPrank(); + + assertEq(timeLock.isOperationDone(id), true); // operation is executed + } + + function test_grantRole() public { + address newProposer = makeAddr("newProposer"); + + bytes memory data = abi.encodeWithSignature( + "grantRole(bytes32,address)", + timeLock.PROPOSER_ROLE(), + newProposer + ); + + address target = address(timeLock); + uint256 value = 0; + bytes32 predecessor = bytes32(0); + bytes32 salt = bytes32(0); + uint256 _delay = 100; + + vm.startPrank(proposer); + timeLock.schedule(target, value, data, predecessor, salt, _delay); + vm.stopPrank(); + + bytes32 id = timeLock.hashOperation( + target, + value, + data, + predecessor, + salt + ); + + assertEq(timeLock.isOperationPending(id), true); // operation is pending + + skip(100); + + vm.startPrank(executor); + timeLock.execute(target, value, data, predecessor, salt); + vm.stopPrank(); + + assertEq(timeLock.isOperationDone(id), true); // operation is executed + assertEq(timeLock.hasRole(timeLock.PROPOSER_ROLE(), newProposer), true); + } +} diff --git a/test/foundry/oracle/SolvBTCBBNOracle.t.sol b/test/foundry/oracle/SolvBTCBBNOracle.t.sol new file mode 100644 index 00000000..6158270f --- /dev/null +++ b/test/foundry/oracle/SolvBTCBBNOracle.t.sol @@ -0,0 +1,24 @@ +pragma solidity ^0.8.10; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import "../../../contracts/oracle/SolvBTCBBNOracle.sol"; + +contract SolvBTCBBNOracleTest is Test { + + SolvBTCBBNOracle oracle; + + uint testnet; + + function setUp() public { + testnet = vm.createSelectFork("https://bsc-testnet.bnbchain.org"); + oracle = SolvBTCBBNOracle(0x3e6c4Efe6D6A470439795756BEDE9f4cd6BdDd5d); + } + + function test_setUp() public { + (bytes32 price, ) = oracle.peek(); + console.logBytes32(price); + } +}