Skip to content

Commit

Permalink
Merge pull request #10 from open-dollar/leverage-mgmt
Browse files Browse the repository at this point in the history
Calculate & Swap
  • Loading branch information
daopunk authored Jun 18, 2024
2 parents 87c7952 + f8732ad commit e30c59f
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 36 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"test:coverage": "forge coverage --report lcov && lcov --ignore-errors unused --remove lcov.info 'node_modules/*' 'script/*' 'test/*' 'src/contracts/for-test/*' 'src/libraries/*' -o lcov.info.pruned && mv lcov.info.pruned lcov.info && genhtml -o coverage-report lcov.info"
},
"dependencies": {
"@opendollar/contracts": "0.0.0-5e2be44d",
"@opendollar/contracts": "0.0.0-e31c2151",
"@openzeppelin/contracts": "4.9.6",
"@paraswap/sdk": "^6.7.0"
},
Expand Down
5 changes: 4 additions & 1 deletion script/Registry.s.sol
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.20;

// ParaSwap Limit Order Contract
// ParaSwap Limit Order Contract (Sol 0.8.1)
address constant PARASWAP_AUGUSTUS_RFQ = 0x0927FD43a7a87E3E8b81Df2c44B03C4756849F6D;

// ParaSwap On-Chain Aggregator (Sol 0.7.5)
address constant PARASWAP_AUGUSTUS_SWAPPER = 0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57;

// Open Dollar
address constant OD_ADDR = 0x221A0f68770658C15B525d0F89F5da2baAB5f321;

Expand Down
81 changes: 81 additions & 0 deletions src/leverage/LeverageCalculator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.20;

import {ISAFEEngine} from '@opendollar/interfaces/ISAFEEngine.sol';
import {IVault721} from '@opendollar/interfaces/proxies/IVault721.sol';
import {IODSafeManager} from '@opendollar/interfaces/proxies/IODSafeManager.sol';
import {Math, RAY} from '@opendollar/libraries/Math.sol';

contract LeverageCalculator {
using Math for uint256;

IVault721 public immutable VAULT721;
ISAFEEngine public immutable SAFEENGINE;

constructor(address _vault721) {
VAULT721 = IVault721(_vault721);
SAFEENGINE = ISAFEEngine(IODSafeManager(VAULT721.safeManager()).safeEngine());
}

/// @dev calculate max single-swap leverage based on initial locked collateral
function calculateSingleLeverage(uint256 _safeId) public view returns (uint256 _leverage) {
(bytes32 _cType, address _safeHandler) = getNFVIds(_safeId);
(uint256 _collateral, uint256 _debt) = getNFVLockedAndDebt(_cType, _safeHandler);
(uint256 _accumulatedRate, uint256 _safetyPrice) = getCData(_cType);

uint256 _maxSafetyDebt = _collateral.wmul(_safetyPrice).wdiv(_accumulatedRate);

if (_maxSafetyDebt > _debt) {
_leverage = _maxSafetyDebt - _debt;
}
}

/// @dev calculate max loop-swap leverage based on initial locked collateral
function calculateMultipleLeverage(uint256 _safeId) external view returns (uint256 _leverage) {
uint256 _accumulator;
uint256 _debtIterator;

_debtIterator = calculateSingleLeverage(_safeId);

while (_debtIterator > 200 ether) {
_accumulator += _debtIterator;
// TODO: recalculate collateral or execute leverage swap
_debtIterator = calculateSingleLeverage(_safeId);
}
}

/// @dev calculate max flashloan leverage based on initial locked collateral
function calculateFlashLeverage(uint256 _safeId) external view returns (uint256 _leverage) {
// TODO: calculate max leverage
return _safeId;
}

/// @return _internalDebt internal account of COIN for an account (internal)
function getCoinBalance(address _proxy) public view returns (uint256 _internalDebt) {
_internalDebt = SAFEENGINE.coinBalance(_proxy) / RAY;
}

/// @dev get cType and safe handler of NFV
function getNFVIds(uint256 _safeId) public view returns (bytes32 _cType, address _safeHandler) {
IVault721.NFVState memory nftState = VAULT721.getNfvState(_safeId);

Check warning on line 60 in src/leverage/LeverageCalculator.sol

View workflow job for this annotation

GitHub Actions / Run Linters (18.x)

'nftState' should start with _
_cType = nftState.cType;
_safeHandler = nftState.safeHandler;
}

/// @dev get locked collateral and generated debt
function getNFVLockedAndDebt(
bytes32 _cType,
address _safeHandler
) public view returns (uint256 _collateral, uint256 _debt) {
ISAFEEngine.SAFE memory _safeData = SAFEENGINE.safes(_cType, _safeHandler);
_collateral = _safeData.lockedCollateral;
_debt = _safeData.generatedDebt;
}

/// @dev get accumulated rate and safety price for a cType
function getCData(bytes32 _cType) public view returns (uint256 _accumulatedRate, uint256 _safetyPrice) {
ISAFEEngine.SAFEEngineCollateralData memory _safeEngCData = SAFEENGINE.cData(_cType);
_accumulatedRate = _safeEngCData.accumulatedRate;
_safetyPrice = _safeEngCData.safetyPrice;
}
}
6 changes: 6 additions & 0 deletions src/leverage/LeverageSwapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.20;

contract LeverageSwapper {
// Todo create and send order to PARASWAP_AUGUSTUS_RFQ
}
34 changes: 33 additions & 1 deletion test/CommonTest.t.sol
Original file line number Diff line number Diff line change
@@ -1,23 +1,52 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.20;

import {Common} from '@opendollar/test/e2e/Common.t.sol';
import {IERC20} from '@openzeppelin/token/ERC20/IERC20.sol';
import {MintableERC20} from '@opendollar/contracts/for-test/MintableERC20.sol';
import {Common, TKN} from '@opendollar/test/e2e/Common.t.sol';
import {Math} from '@opendollar/libraries/Math.sol';
import {ODProxy} from '@opendollar/contracts/proxies/ODProxy.sol';
import {IVault721} from '@opendollar/interfaces/proxies/IVault721.sol';
import {ISAFEEngine} from '@opendollar/interfaces/ISAFEEngine.sol';
import {ExitActions} from 'src/leverage/ExitActions.sol';
import {LeverageCalculator} from 'src/leverage/LeverageCalculator.sol';

contract CommonTest is Common {
using Math for uint256;

uint256 public constant DEPOSIT = 10_000 ether;
uint256 public constant MINT = DEPOSIT * 2 / 3;

address public token;

address public aliceProxy;
address public bobProxy;
address public deployerProxy;

IVault721.NFVState public aliceNFV;

ExitActions public exitActions;
LeverageCalculator public leverageCalculator;

mapping(address proxy => uint256 safeId) public vaults;

function setUp() public virtual override {
super.setUp();
exitActions = new ExitActions();
leverageCalculator = new LeverageCalculator(address(vault721));
token = address(collateral[TKN]);

aliceProxy = _deployOrFind(alice);
_openSafe(aliceProxy, TKN);

MintableERC20(token).mint(alice, DEPOSIT);

vm.prank(alice);
IERC20(token).approve(aliceProxy, type(uint256).max);

aliceNFV = vault721.getNfvState(vaults[aliceProxy]);
}

function _deployOrFind(address _owner) internal returns (address _proxy) {
_proxy = vault721.getProxy(_owner);
if (_proxy == address(0)) {
Expand Down Expand Up @@ -51,6 +80,7 @@ contract CommonTest is Common {
exitActions.generateDebtToAccount.selector, _contract, address(safeManager), address(coinJoin), _safeId, _deltaWad
);
ODProxy(_proxy).execute(address(exitActions), _payload);
vm.stopPrank();
}

function _genDebtToProxy(uint256 _safeId, uint256 _deltaWad, address _proxy) internal {
Expand All @@ -59,13 +89,15 @@ contract CommonTest is Common {
exitActions.generateDebtToProxy.selector, address(safeManager), address(coinJoin), _safeId, _deltaWad
);
ODProxy(_proxy).execute(address(exitActions), _payload);
vm.stopPrank();
}

function _genInternalDebt(uint256 _safeId, uint256 _deltaWad, address _proxy) internal {
vm.startPrank(ODProxy(_proxy).OWNER());
bytes memory _payload =
abi.encodeWithSelector(exitActions.generateInternalDebt.selector, address(safeManager), _safeId, _deltaWad);
ODProxy(_proxy).execute(address(exitActions), _payload);
vm.stopPrank();
}

function _depositCollateralAndGenDebt(
Expand Down
25 changes: 0 additions & 25 deletions test/e2e/E2ECoinExit.t.sol
Original file line number Diff line number Diff line change
@@ -1,37 +1,12 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.20;

import {IERC20} from '@openzeppelin/token/ERC20/IERC20.sol';
import {MintableERC20} from '@opendollar/contracts/for-test/MintableERC20.sol';
import {IVault721} from '@opendollar/interfaces/proxies/IVault721.sol';
import {RAY} from '@opendollar/libraries/Math.sol';
import {TKN} from '@opendollar/test/e2e/Common.t.sol';
import {ExitActions} from 'src/leverage/ExitActions.sol';
import {CommonTest} from 'test/CommonTest.t.sol';

contract E2ECoinExit is CommonTest {
uint256 public constant DEPOSIT = 10_000 ether;
uint256 public constant MINT = DEPOSIT * 2 / 3;

address public arbitraryContract = address(0x1234abcd);
address public token;
IVault721.NFVState public aliceNFV;

function setUp() public virtual override {
super.setUp();
exitActions = new ExitActions();
token = address(collateral[TKN]);

aliceProxy = _deployOrFind(alice);
_openSafe(aliceProxy, TKN);

MintableERC20(token).mint(alice, DEPOSIT);

vm.prank(alice);
IERC20(token).approve(aliceProxy, type(uint256).max);

aliceNFV = vault721.getNfvState(vaults[aliceProxy]);
}

function testLockCollateral() public {
(uint256 _c1, uint256 _d1) = _getSAFE(TKN, aliceNFV.safeHandler);
Expand Down
134 changes: 134 additions & 0 deletions test/e2e/LeverageCalculator.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.20;

import {IERC20} from '@openzeppelin/token/ERC20/IERC20.sol';
import {MintableERC20} from '@opendollar/contracts/for-test/MintableERC20.sol';
import {TKN} from '@opendollar/test/e2e/Common.t.sol';
import {CommonTest} from 'test/CommonTest.t.sol';

contract E2ELeverageCalculator is CommonTest {
address public constant LEVERAGE_HANDLER = address(0x1111);
address public leverageHandlerProxy;

function setUp() public virtual override {
super.setUp();
MintableERC20(token).mint(address(this), DEPOSIT);

leverageHandlerProxy = _deployOrFind(LEVERAGE_HANDLER);

vm.prank(LEVERAGE_HANDLER);
IERC20(token).approve(leverageHandlerProxy, type(uint256).max);

vm.prank(aliceProxy);
safeManager.allowSAFE(vaults[aliceProxy], leverageHandlerProxy, true);
}

function testLockCollateral() public {
/// @notice collateral locked
_lockCollateral(TKN, vaults[aliceProxy], DEPOSIT, aliceProxy);
(uint256 _collateral,) = leverageCalculator.getNFVLockedAndDebt(TKN, aliceNFV.safeHandler);
assertEq(_collateral, DEPOSIT);
}

/// @notice exited coins transfered to proxy
function testLockAndGenerateDebtToProxy() public {
_lockCollateral(TKN, vaults[aliceProxy], DEPOSIT, aliceProxy);
_genDebtToProxy(vaults[aliceProxy], MINT, aliceProxy);
(, uint256 _debt) = leverageCalculator.getNFVLockedAndDebt(TKN, aliceNFV.safeHandler);
assertEq(_debt, MINT);
uint256 _internalDebt = leverageCalculator.getCoinBalance(aliceProxy);
assertEq(_internalDebt, 0);
}

/// @notice internal coins are available to proxy
function testLockAndGenerateInternalDebt() public {
_lockCollateral(TKN, vaults[aliceProxy], DEPOSIT, aliceProxy);
_genInternalDebt(vaults[aliceProxy], MINT, aliceProxy);
(, uint256 _debt) = leverageCalculator.getNFVLockedAndDebt(TKN, aliceNFV.safeHandler);
assertEq(_debt, MINT);
uint256 _internalDebt = leverageCalculator.getCoinBalance(aliceProxy);
assertEq(_internalDebt, _debt);
}

/// @notice 1/2 internal coins were exited to proxy
function testLockAndGenerateInternalDebtAndPartialExitToProxy() public {
_lockCollateral(TKN, vaults[aliceProxy], DEPOSIT, aliceProxy);
_genInternalDebt(vaults[aliceProxy], MINT, aliceProxy);
assertEq(systemCoin.balanceOf(aliceProxy), 0);

_exitCoinToAccount(aliceProxy, aliceProxy, MINT / 2);
(, uint256 _debt) = leverageCalculator.getNFVLockedAndDebt(TKN, aliceNFV.safeHandler);
assertEq(_debt, MINT);

uint256 _internalDebt = leverageCalculator.getCoinBalance(aliceProxy);
assertEq(_internalDebt, _debt / 2);
assertEq(systemCoin.balanceOf(aliceProxy), MINT / 2);
}

/// @notice leverageAmount used to make swap and leverage safe
function testCalcSingleSwap() public {
_lockCollateral(TKN, vaults[aliceProxy], DEPOSIT, aliceProxy);
(uint256 _initCollateral,) = leverageCalculator.getNFVLockedAndDebt(TKN, aliceNFV.safeHandler);

uint256 _leverageAmount = leverageCalculator.calculateSingleLeverage(vaults[aliceProxy]);
_genDebtToAccount(LEVERAGE_HANDLER, vaults[aliceProxy], _leverageAmount, aliceProxy);
assertEq(systemCoin.balanceOf(LEVERAGE_HANDLER), _leverageAmount);

vm.prank(LEVERAGE_HANDLER);
_swapIn(LEVERAGE_HANDLER, _leverageAmount);
_swapOut(LEVERAGE_HANDLER);

assertEq(systemCoin.balanceOf(LEVERAGE_HANDLER), 0);
assertEq(IERC20(token).balanceOf(LEVERAGE_HANDLER), _leverageAmount);

vm.prank(LEVERAGE_HANDLER);
_lockCollateral(TKN, vaults[aliceProxy], _leverageAmount, leverageHandlerProxy);
(uint256 _finalCollateral,) = leverageCalculator.getNFVLockedAndDebt(TKN, aliceNFV.safeHandler);

/// @dev this maths because collateral and debt tokens are equal: 1-to-1
assertEq(_initCollateral + _leverageAmount, _finalCollateral);
}

/// @notice (leverageAmount - initial debt) used to make swap and leverage safe
function testCalcSingleSwapWithPreviousDebtMinted() public {
_lockCollateral(TKN, vaults[aliceProxy], DEPOSIT, aliceProxy);
(uint256 _initCollateral,) = leverageCalculator.getNFVLockedAndDebt(TKN, aliceNFV.safeHandler);
uint256 _leverageAmount = leverageCalculator.calculateSingleLeverage(vaults[aliceProxy]);

_genDebtToAccount(address(0x9999), vaults[aliceProxy], MINT / 2, aliceProxy);
uint256 _newLeverageAmount = leverageCalculator.calculateSingleLeverage(vaults[aliceProxy]);
assertEq(_newLeverageAmount, _leverageAmount - MINT / 2);

_genDebtToAccount(LEVERAGE_HANDLER, vaults[aliceProxy], _newLeverageAmount, aliceProxy);
assertEq(systemCoin.balanceOf(LEVERAGE_HANDLER), _newLeverageAmount);

vm.prank(LEVERAGE_HANDLER);
_swapIn(LEVERAGE_HANDLER, _newLeverageAmount);
_swapOut(LEVERAGE_HANDLER);

assertEq(systemCoin.balanceOf(LEVERAGE_HANDLER), 0);
assertEq(IERC20(token).balanceOf(LEVERAGE_HANDLER), _newLeverageAmount);

vm.prank(LEVERAGE_HANDLER);
_lockCollateral(TKN, vaults[aliceProxy], _newLeverageAmount, leverageHandlerProxy);
(uint256 _finalCollateral,) = leverageCalculator.getNFVLockedAndDebt(TKN, aliceNFV.safeHandler);

/// @dev this maths because collateral and debt tokens are equal: 1-to-1
assertEq(_initCollateral + _newLeverageAmount, _finalCollateral);
}

/// SIMULATION FUNCTIONS
mapping(address => uint256) public swapIn;

/**
* @dev to simulate swaps in tests
*/
function _swapIn(address _sender, uint256 _amount) internal {
require(systemCoin.transfer(address(this), _amount), 'SwapInFail');

Check warning on line 127 in test/e2e/LeverageCalculator.t.sol

View workflow job for this annotation

GitHub Actions / Run Linters (18.x)

GC: Use Custom Errors instead of require statements
swapIn[_sender] = _amount;
}

function _swapOut(address _receiver) internal {
IERC20(token).transfer(_receiver, swapIn[_receiver]);
}
}
16 changes: 8 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"

"@opendollar/[email protected]5e2be44d":
version "0.0.0-5e2be44d"
resolved "https://registry.yarnpkg.com/@opendollar/contracts/-/contracts-0.0.0-5e2be44d.tgz#ce0b878acc42fade52665e8835236cceddedf7a6"
integrity sha512-A3luZUGi5au79JVqERpbO1jJ/MaGfIZUZJzuQ3a3DzqLa3HU0mbVHu4S93D0mmwvw7TJPIr1vuRLYdhYlR4vYQ==
"@opendollar/[email protected]e31c2151":
version "0.0.0-e31c2151"
resolved "https://registry.yarnpkg.com/@opendollar/contracts/-/contracts-0.0.0-e31c2151.tgz#d96fea17c8f865f6b35c74a80d0e95cdfed10943"
integrity sha512-mffuIIDH6YV7gxEbUQghPtKvCamcFoFG4YWnWozjtDec/vrusm2TCjrAKvzV9v8+Sha0dkr6ROryfyMxZIH+IQ==
dependencies:
"@defi-wonderland/solidity-utils" "0.0.0-4298c6c6"
"@openzeppelin/contracts" "4.9.6"
Expand Down Expand Up @@ -765,14 +765,14 @@ flatted@^2.0.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==

"forge-std@git+https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e":
version "1.5.6"
resolved "git+https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e"

"forge-std@https://github.com/foundry-rs/forge-std":
version "1.8.2"
resolved "https://github.com/foundry-rs/forge-std#52715a217dc51d0de15877878ab8213f6cbbbab5"

"forge-std@https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e":
version "1.5.6"
resolved "https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e"

form-data-encoder@^2.1.2:
version "2.1.4"
resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5"
Expand Down

0 comments on commit e30c59f

Please sign in to comment.