Skip to content

Commit

Permalink
DYDXWrapper drafted
Browse files Browse the repository at this point in the history
  • Loading branch information
alcueca committed Jul 25, 2023
1 parent d52f72d commit 243aae4
Show file tree
Hide file tree
Showing 7 changed files with 402 additions and 4 deletions.
220 changes: 220 additions & 0 deletions src/dydx/DYDXWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Derived from https://github.com/kollateral/kollateral/blob/master/protocol/contracts/liquidity/kollateral/KollateralLiquidityProxy.sol
pragma solidity ^0.8.0;

import { SoloMarginLike } from "./interfaces/SoloMarginLike.sol";
import { DYDXFlashBorrowerLike } from "./interfaces/DYDXFlashBorrowerLike.sol";
import { DYDXDataTypes } from "./libraries/DYDXDataTypes.sol";
import { RevertMsgExtractor } from "../utils/RevertMsgExtractor.sol";

import { IERC20 } from "lib/erc3156pp/src/interfaces/IERC20.sol";
import { IERC3156PPFlashLender } from "lib/erc3156pp/src/interfaces/IERC3156PPFlashLender.sol";


library TransferHelper {
/// @notice Transfers tokens from msg.sender to a recipient
/// @dev Errors with the underlying revert message if transfer fails
/// @param token The contract address of the token which will be transferred
/// @param to The recipient of the transfer
/// @param value The value of the transfer
function safeTransfer(
IERC20 token,
address to,
uint256 value
) internal {
(bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(IERC20.transfer.selector, to, value));
if (!(success && (data.length == 0 || abi.decode(data, (bool))))) revert(RevertMsgExtractor.getRevertMsg(data));
}
}


contract DYDXWrapper is IERC3156PPFlashLender, DYDXFlashBorrowerLike {
using TransferHelper for IERC20;

uint256 internal NULL_ACCOUNT_ID = 0;
uint256 internal NULL_MARKET_ID = 0;
DYDXDataTypes.AssetAmount internal NULL_AMOUNT = DYDXDataTypes.AssetAmount({
sign: false,
denomination: DYDXDataTypes.AssetDenomination.Wei,
ref: DYDXDataTypes.AssetReference.Delta,
value: 0
});
bytes internal NULL_DATA = "";
bytes internal _callbackResult;

SoloMarginLike public soloMargin;
mapping(IERC20 => uint256) public assetAddressToMarketId;
mapping(IERC20 => bool) public assetsRegistered;

/// @param soloMargin_ DYDX SoloMargin address
constructor (SoloMarginLike soloMargin_) {
soloMargin = soloMargin_;

for (uint256 marketId = 0; marketId <= 3; marketId++) {
IERC20 asset = IERC20(soloMargin.getMarketTokenAddress(marketId));
assetAddressToMarketId[asset] = marketId;
assetsRegistered[asset] = true;
}
}

/**
* @dev From ERC-3156++. The fee to be charged for a given loan.
* @param asset The loan currency.
* @param amount The amount of assets lent.
* @return The amount of `asset` to be charged for the loan, on top of the returned principal.
*/
function flashFee(IERC20 asset, uint256 amount) public view returns (uint256) {
require(assetsRegistered[asset], "Unsupported currency");
return (amount <= asset.balanceOf(address(soloMargin))) ? 2 : type(uint256).max;
}

/// @dev Use the aggregator to serve an ERC3156++ flash loan.
/// @dev Forward the callback to the callback receiver. The borrower only needs to trust the aggregator and its governance, instead of the underlying lenders.
/// @param loanReceiver The address receiving the flash loan
/// @param asset The asset to be loaned
/// @param amount The amount to loaned
/// @param userData The ABI encoded user data
/// @param callback The address and signature of the callback function
/// @return result ABI encoded result of the callback
function flashLoan(
address loanReceiver,
IERC20 asset,
uint256 amount,
bytes calldata userData,
/// @dev callback.
/// This is a concatenation of (address, bytes4), where the address is the callback receiver, and the bytes4 is the signature of callback function.
/// The arguments in the callback function are fixed.
/// If the callback receiver needs to know the loan receiver, it should be encoded by the initiator in `data`.
/// @param initiator The address that called this function
/// @param paymentReceiver The address that needs to receive the amount plus fee at the end of the callback
/// @param asset The asset to be loaned
/// @param amount The amount to loaned
/// @param fee The fee to be paid
/// @param data The ABI encoded data to be passed to the callback
/// @return result ABI encoded result of the callback
function(address, address, IERC20, uint256, uint256, bytes memory) external returns (bytes memory) callback
) external returns (bytes memory) {
DYDXDataTypes.ActionArgs[] memory operations = new DYDXDataTypes.ActionArgs[](3);
operations[0] = getWithdrawAction(asset, amount);
operations[1] = getCallAction(abi.encode(msg.sender, loanReceiver, asset, amount, callback.address, callback.selector, userData));
operations[2] = getDepositAction(asset, amount + flashFee(asset, amount));
DYDXDataTypes.AccountInfo[] memory accountInfos = new DYDXDataTypes.AccountInfo[](1);
accountInfos[0] = getAccountInfo();

soloMargin.operate(accountInfos, operations);

bytes memory result = _callbackResult;
_callbackResult = ""; // TODO: Confirm that this deletes the storage variable
return result;
}

/// @dev DYDX flash loan callback. It sends the value borrowed to `receiver`, and takes it back plus a `flashFee` after the ERC3156 callback.
function callFunction(
address sender,
DYDXDataTypes.AccountInfo memory,
bytes memory data
)
public override
{
require(msg.sender == address(soloMargin), "Callback only from SoloMargin");
require(sender == address(this), "FlashLoan only from this contract");

// We pass the loan to the loan receiver and we store the callback result in storage for the the ERC3156++ flashLoan function to recover it.
_callbackResult = _callFromData(data);
}


/// @dev Internal function to transfer to the loan receiver and the callback. It is used to avoid stack too deep.
function _callFromData(bytes memory data) internal returns(bytes memory) {
(address initiator, address loanReceiver, IERC20 asset, uint256 amount, address callbackReceiver, bytes4 callbackSelector, bytes memory userData) =
abi.decode(data, (address, address, IERC20, uint256, address, bytes4, bytes));

uint256 fee = flashFee(asset, amount);

// We pass the loan to the loan receiver
asset.safeTransfer(loanReceiver, amount);
(bool success, bytes memory result) = callbackReceiver.call(abi.encodeWithSelector(
callbackSelector,
initiator, // initiator
address(this), // paymentReceiver
asset, // asset
amount, // amount
fee, // fee
userData // data
));
if(!success) revert(RevertMsgExtractor.getRevertMsg(result));

// Approve the SoloMargin contract allowance to *pull* the owed amount
IERC20(asset).approve(address(soloMargin), amount + fee);

return abi.decode(result, (bytes));
}

function getAccountInfo() internal view returns (DYDXDataTypes.AccountInfo memory) {
return DYDXDataTypes.AccountInfo({
owner: address(this),
number: 1
});
}

function getWithdrawAction(IERC20 asset, uint256 amount)
internal
view
returns (DYDXDataTypes.ActionArgs memory)
{
return DYDXDataTypes.ActionArgs({
actionType: DYDXDataTypes.ActionType.Withdraw,
accountId: 0,
amount: DYDXDataTypes.AssetAmount({
sign: false,
denomination: DYDXDataTypes.AssetDenomination.Wei,
ref: DYDXDataTypes.AssetReference.Delta,
value: amount
}),
primaryMarketId: assetAddressToMarketId[asset],
secondaryMarketId: NULL_MARKET_ID,
otherAddress: address(this), // TODO: Would this send the assets straight to `loanReceiver`?
otherAccountId: NULL_ACCOUNT_ID,
data: NULL_DATA
});
}

function getDepositAction(IERC20 asset, uint256 repaymentAmount)
internal
view
returns (DYDXDataTypes.ActionArgs memory)
{
return DYDXDataTypes.ActionArgs({
actionType: DYDXDataTypes.ActionType.Deposit,
accountId: 0,
amount: DYDXDataTypes.AssetAmount({
sign: true,
denomination: DYDXDataTypes.AssetDenomination.Wei,
ref: DYDXDataTypes.AssetReference.Delta,
value: repaymentAmount
}),
primaryMarketId: assetAddressToMarketId[asset],
secondaryMarketId: NULL_MARKET_ID,
otherAddress: address(this),
otherAccountId: NULL_ACCOUNT_ID,
data: NULL_DATA
});
}

function getCallAction(bytes memory data_)
internal
view
returns (DYDXDataTypes.ActionArgs memory)
{
return DYDXDataTypes.ActionArgs({
actionType: DYDXDataTypes.ActionType.Call,
accountId: 0,
amount: NULL_AMOUNT,
primaryMarketId: NULL_MARKET_ID,
secondaryMarketId: NULL_MARKET_ID,
otherAddress: address(this),
otherAccountId: NULL_ACCOUNT_ID,
data: data_
});
}
}
29 changes: 29 additions & 0 deletions src/dydx/interfaces/DYDXFlashBorrowerLike.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.0;

import "../libraries/DYDXDataTypes.sol";

/**
* @title DYDXFlashBorrowerLike
* @author dYdX
*
* Interface that Callees for Solo must implement in order to ingest data.
*/
interface DYDXFlashBorrowerLike {

// ============ Public Functions ============

/**
* Allows users to send this contract arbitrary data.
*
* @param sender The msg.sender to Solo
* @param accountInfo The account from which the data is being sent
* @param data Arbitrary data given by the sender
*/
function callFunction(
address sender,
DYDXDataTypes.AccountInfo memory accountInfo,
bytes memory data
)
external;
}
9 changes: 9 additions & 0 deletions src/dydx/interfaces/SoloMarginLike.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.0;

import "../libraries/DYDXDataTypes.sol";

interface SoloMarginLike {
function operate(DYDXDataTypes.AccountInfo[] memory accounts, DYDXDataTypes.ActionArgs[] memory actions) external;
function getMarketTokenAddress(uint256 marketId) external view returns (address);
}
55 changes: 55 additions & 0 deletions src/dydx/libraries/DYDXDataTypes.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Derived from https://github.com/kollateral/kollateral
pragma solidity ^0.8.0;

library DYDXDataTypes {
enum ActionType {
Deposit, // supply tokens
Withdraw, // flashLoan tokens
Transfer, // transfer balance between accounts
Buy, // buy an amount of some token (externally)
Sell, // sell an amount of some token (externally)
Trade, // trade tokens against another account
Liquidate, // liquidate an undercollateralized or expiring account
Vaporize, // use excess tokens to zero-out a completely negative account
Call // send arbitrary data to an address
}

enum AssetDenomination {
Wei, // the amount is denominated in wei
Par // the amount is denominated in par
}

enum AssetReference {
Delta, // the amount is given as a delta from the current value
Target // the amount is given as an exact number to end up at
}

struct AssetAmount {
bool sign; // true if positive
AssetDenomination denomination;
AssetReference ref;
uint256 value;
}

struct Wei {
bool sign; // true if positive
uint256 value;
}

struct ActionArgs {
ActionType actionType;
uint256 accountId;
AssetAmount amount;
uint256 primaryMarketId;
uint256 secondaryMarketId;
address otherAddress;
uint256 otherAccountId;
bytes data;
}

struct AccountInfo {
address owner; // The address that owns the account
uint256 number; // A nonce that allows a single address to control many accounts
}
}
Loading

0 comments on commit 243aae4

Please sign in to comment.