diff --git a/src/dydx/DYDXWrapper.sol b/src/dydx/DYDXWrapper.sol new file mode 100644 index 0000000..7ff9a78 --- /dev/null +++ b/src/dydx/DYDXWrapper.sol @@ -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_ + }); + } +} \ No newline at end of file diff --git a/src/dydx/interfaces/DYDXFlashBorrowerLike.sol b/src/dydx/interfaces/DYDXFlashBorrowerLike.sol new file mode 100644 index 0000000..5dd66c8 --- /dev/null +++ b/src/dydx/interfaces/DYDXFlashBorrowerLike.sol @@ -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; +} \ No newline at end of file diff --git a/src/dydx/interfaces/SoloMarginLike.sol b/src/dydx/interfaces/SoloMarginLike.sol new file mode 100644 index 0000000..284a0b9 --- /dev/null +++ b/src/dydx/interfaces/SoloMarginLike.sol @@ -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); +} diff --git a/src/dydx/libraries/DYDXDataTypes.sol b/src/dydx/libraries/DYDXDataTypes.sol new file mode 100644 index 0000000..24f7c60 --- /dev/null +++ b/src/dydx/libraries/DYDXDataTypes.sol @@ -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 + } +} diff --git a/src/dydx/mocks/SoloMarginMock.sol b/src/dydx/mocks/SoloMarginMock.sol new file mode 100644 index 0000000..b945b60 --- /dev/null +++ b/src/dydx/mocks/SoloMarginMock.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Derived from https://github.com/kollateral/kollateral +pragma solidity ^0.8.0; + +import { IERC20 } from "lib/erc3156pp/src/interfaces/IERC20.sol"; +import "../interfaces/SoloMarginLike.sol"; +import "../interfaces/DYDXFlashBorrowerLike.sol"; + +contract SoloMarginMock is SoloMarginLike { + + mapping(uint256 => address) internal _markets; + + constructor(uint256[] memory marketIds, address[] memory tokenAddresses) { + for (uint256 i = 0; i < marketIds.length; i++) { + _markets[marketIds[i]] = tokenAddresses[i]; + } + } + + function operate(DYDXDataTypes.AccountInfo[] memory accounts, DYDXDataTypes.ActionArgs[] memory actions) public override { + /* data */ + require(accounts.length == 1, "SoloMarginMock: incorrect accounts length"); + require(actions.length == 3, "SoloMarginMock: incorrect actions length"); + + /* withdraw */ + DYDXDataTypes.ActionArgs memory withdraw = actions[0]; + + require(withdraw.amount.sign == false, "SoloMarginMock: incorrect withdraw sign"); + require(withdraw.amount.denomination == DYDXDataTypes.AssetDenomination.Wei, "SoloMarginMock: incorrect withdraw denomination"); + require(withdraw.amount.ref == DYDXDataTypes.AssetReference.Delta, "SoloMarginMock: incorrect withdraw reference"); + + require(withdraw.actionType == DYDXDataTypes.ActionType.Withdraw, "SoloMarginMock: incorrect withdraw action type"); + + /* call */ + DYDXDataTypes.ActionArgs memory call = actions[1]; + require(call.actionType == DYDXDataTypes.ActionType.Call, "SoloMarginMock: incorrect call action type"); + + /* deposit */ + DYDXDataTypes.ActionArgs memory deposit = actions[2]; + require(withdraw.primaryMarketId == deposit.primaryMarketId, "SoloMarginMock: marketId mismatch"); + + uint256 depositValue = withdraw.amount.value + repaymentFee(withdraw.primaryMarketId); + require(deposit.amount.value == depositValue, "SoloMarginMock: incorrect deposit value"); + require(deposit.amount.sign == true, "SoloMarginMock: incorrect deposit sign"); + require(deposit.amount.denomination == DYDXDataTypes.AssetDenomination.Wei, "SoloMarginMock: incorrect deposit denomination"); + require(deposit.amount.ref == DYDXDataTypes.AssetReference.Delta, "SoloMarginMock: incorrect deposit reference"); + + require(deposit.actionType == DYDXDataTypes.ActionType.Deposit, "SoloMarginMock: incorrect deposit action type"); + + uint256 balanceBefore = balanceOf(withdraw.primaryMarketId); + + transfer(withdraw.primaryMarketId, msg.sender, withdraw.amount.value); + + DYDXFlashBorrowerLike(msg.sender).callFunction(msg.sender, DYDXDataTypes.AccountInfo({ + owner: accounts[0].owner, + number: accounts[0].number + }), call.data); + + transferFrom(deposit.primaryMarketId, msg.sender, address(this), deposit.amount.value); + uint256 balanceAfter = balanceOf(withdraw.primaryMarketId); + + require(balanceAfter == balanceBefore + repaymentFee(withdraw.primaryMarketId), "SoloMarginMock: Incorrect ending balance"); + } + + function getMarketTokenAddress(uint256 marketId) public view override returns (address) { + return _markets[marketId]; + } + + function repaymentFee(uint256) internal pure returns (uint256) { + return 2; + } + + function transfer(uint256 marketId, address to, uint256 amount) internal returns (bool) { + return IERC20(_markets[marketId]).transfer(to, amount); + } + + function transferFrom(uint256 marketId, address from, address to, uint256 amount) internal returns (bool) { + return IERC20(_markets[marketId]).transferFrom(from, to, amount); + } + + function balanceOf(uint256 marketId) internal view returns (uint256) { + return IERC20(_markets[marketId]).balanceOf(address(this)); + } +} diff --git a/src/erc3156/ERC3156Wrapper.sol b/src/erc3156/ERC3156Wrapper.sol index 0e1de0d..a9144d5 100644 --- a/src/erc3156/ERC3156Wrapper.sol +++ b/src/erc3156/ERC3156Wrapper.sol @@ -100,7 +100,9 @@ contract ERC3156Wrapper is IERC3156PPFlashLender, IERC3156FlashBorrower { // We get funds from an ERC3156 lender to serve the ERC3156++ flash loan in our ERC3156 callback lender.flashLoan(this, address(asset), amount, data); - return _callbackResult; + bytes memory result = _callbackResult; + _callbackResult = ""; // TODO: Confirm that this deletes the storage variable + return result; } /** diff --git a/src/test/FlashBorrower.sol b/src/test/FlashBorrower.sol index 8d55755..fe1b220 100644 --- a/src/test/FlashBorrower.sol +++ b/src/test/FlashBorrower.sol @@ -28,7 +28,7 @@ contract FlashBorrower { } /// @dev ERC-3156++ Flash loan callback - function onFlashLoan(address initiator, address paymentReceiver, IERC20 asset, uint256 amount, uint256 fee, bytes calldata data) external returns(bytes memory) { + function onFlashLoan(address initiator, address paymentReceiver, IERC20 asset, uint256 amount, uint256 fee, bytes calldata) external returns(bytes memory) { require(msg.sender == address(lender), "FlashBorrower: Untrusted lender"); require(initiator == address(this), "FlashBorrower: External loan initiator"); @@ -43,7 +43,7 @@ contract FlashBorrower { return abi.encode(ERC3156PP_CALLBACK_SUCCESS); } - function onSteal(address initiator, address paymentReceiver, IERC20 asset, uint256 amount, uint256 fee, bytes calldata data) external returns(bytes memory) { + function onSteal(address initiator, address, IERC20 asset, uint256 amount, uint256 fee, bytes calldata) external returns(bytes memory) { require(msg.sender == address(lender), "FlashBorrower: Untrusted lender"); require(initiator == address(this), "FlashBorrower: External loan initiator"); flashInitiator = initiator; @@ -56,7 +56,7 @@ contract FlashBorrower { return abi.encode(ERC3156PP_CALLBACK_SUCCESS); } - function onReenter(address initiator, address paymentReceiver, IERC20 asset, uint256 amount, uint256 fee, bytes calldata data) external returns(bytes memory) { + function onReenter(address initiator, address paymentReceiver, IERC20 asset, uint256 amount, uint256 fee, bytes calldata) external returns(bytes memory) { require(msg.sender == address(lender), "FlashBorrower: Untrusted lender"); require(initiator == address(this), "FlashBorrower: External loan initiator"); flashInitiator = initiator;