Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A single-file simplest possible template for a contract that obtains a flash loan from dydx, does things, and pays it back.
// SPDX-License-Identifier: AGPL-3.0-or-later
// The ABI encoder is necessary, but older Solidity versions should work
pragma solidity ^0.7.0;
pragma experimental ABIEncoderV2;
// These definitions are taken from across multiple dydx contracts, and are
// limited to just the bare minimum necessary to make flash loans work.
library Types {
enum AssetDenomination { Wei, Par }
enum AssetReference { Delta, Target }
struct AssetAmount {
bool sign;
AssetDenomination denomination;
AssetReference ref;
uint256 value;
}
}
library Account {
struct Info {
address owner;
uint256 number;
}
}
library Actions {
enum ActionType {
Deposit, Withdraw, Transfer, Buy, Sell, Trade, Liquidate, Vaporize, Call
}
struct ActionArgs {
ActionType actionType;
uint256 accountId;
Types.AssetAmount amount;
uint256 primaryMarketId;
uint256 secondaryMarketId;
address otherAddress;
uint256 otherAccountId;
bytes data;
}
}
interface ISoloMargin {
function operate(Account.Info[] memory accounts, Actions.ActionArgs[] memory actions) external;
}
// The interface for a contract to be callable after receiving a flash loan
interface ICallee {
function callFunction(address sender, Account.Info memory accountInfo, bytes memory data) external;
}
// Standard ERC-20 interface
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
// Additional methods available for WETH
interface IWETH is IERC20 {
function deposit() external payable;
function withdraw(uint wad) external;
}
contract FlashLoanTemplate is ICallee {
// The WETH token contract, since we're assuming we want a loan in WETH
IWETH private WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
// The dydx Solo Margin contract, as can be found here:
// https://github.com/dydxprotocol/solo/blob/master/migrations/deployed.json
ISoloMargin private soloMargin = ISoloMargin(0x1E0447b19BB6EcFdAe1e4AE1694b0C3659614e4e);
constructor() {
// Give infinite approval to dydx to withdraw WETH on contract deployment,
// so we don't have to approve the loan repayment amount (+2 wei) on each call.
// The approval is used by the dydx contract to pay the loan back to itself.
WETH.approve(address(soloMargin), uint(-1));
}
// This is the function we call
function flashLoan(uint loanAmount) external {
/*
The flash loan functionality in dydx is predicated by their "operate" function,
which takes a list of operations to execute, and defers validating the state of
things until it's done executing them.
We thus create three operations, a Withdraw (which loans us the funds), a Call
(which invokes the callFunction method on this contract), and a Deposit (which
repays the loan, plus the 2 wei fee), and pass them all to "operate".
Note that the Deposit operation will invoke the transferFrom to pay the loan
(or whatever amount it was initialised with) back to itself, there is no need
to pay it back explicitly.
The loan must be given as an ERC-20 token, so WETH is used instead of ETH. Other
currencies (DAI, USDC) are also available, their index can be looked up by
calling getMarketTokenAddress on the solo margin contract, and set as the
primaryMarketId in the Withdraw and Deposit definitions.
*/
Actions.ActionArgs[] memory operations = new Actions.ActionArgs[](3);
operations[0] = Actions.ActionArgs({
actionType: Actions.ActionType.Withdraw,
accountId: 0,
amount: Types.AssetAmount({
sign: false,
denomination: Types.AssetDenomination.Wei,
ref: Types.AssetReference.Delta,
value: loanAmount // Amount to borrow
}),
primaryMarketId: 0, // WETH
secondaryMarketId: 0,
otherAddress: address(this),
otherAccountId: 0,
data: ""
});
operations[1] = Actions.ActionArgs({
actionType: Actions.ActionType.Call,
accountId: 0,
amount: Types.AssetAmount({
sign: false,
denomination: Types.AssetDenomination.Wei,
ref: Types.AssetReference.Delta,
value: 0
}),
primaryMarketId: 0,
secondaryMarketId: 0,
otherAddress: address(this),
otherAccountId: 0,
data: abi.encode(
// Replace or add any additional variables that you want
// to be available to the receiver function
msg.sender,
loanAmount
)
});
operations[2] = Actions.ActionArgs({
actionType: Actions.ActionType.Deposit,
accountId: 0,
amount: Types.AssetAmount({
sign: true,
denomination: Types.AssetDenomination.Wei,
ref: Types.AssetReference.Delta,
value: loanAmount + 2 // Repayment amount with 2 wei fee
}),
primaryMarketId: 0, // WETH
secondaryMarketId: 0,
otherAddress: address(this),
otherAccountId: 0,
data: ""
});
Account.Info[] memory accountInfos = new Account.Info[](1);
accountInfos[0] = Account.Info({owner: address(this), number: 1});
soloMargin.operate(accountInfos, operations);
}
// This is the function called by dydx after giving us the loan
function callFunction(address sender, Account.Info memory accountInfo, bytes memory data) external override {
// Decode the passed variables from the data object
(
// This must match the variables defined in the Call object above
address payable actualSender,
uint loanAmount
) = abi.decode(data, (
address, uint
));
// We now have a WETH balance of loanAmount. The logic for what we
// want to do with it goes here. The code below is just there in case
// it's useful.
// It can be useful for debugging to have a verbose error message when
// the loan can't be paid, since dydx doesn't provide one
require(WETH.balanceOf(address(this)) > loanAmount + 2, "CANNOT REPAY LOAN");
// Leave just enough WETH to pay back the loan, and convert the rest to ETH
WETH.withdraw(WETH.balanceOf(address(this)) - loanAmount - 2);
// Send any profit in ETH to the account that invoked this transaction
actualSender.transfer(address(this).balance);
}
}
@JamesHayslip
Copy link

JamesHayslip commented Apr 3, 2021

Hi, just came across this and think it's great! Thanks for it. I have a question though. In the flashloan function I see it collecting data for a withdraw, call and deposit and finishes with soloMargin.operate(). Is the 'callFunction' called from here? I'm assuming it is during operations[1] and once complete it returns the load in operations[2]? Thanks for helping out a newbie.

@Oliver917
Copy link

Oliver917 commented Apr 29, 2021

Hi @cryptoscopia , I'm running into an issue, hope someone can help.

I've replaced the WETH and Solo contract for Kovan, the rest of the contract is original.

    IWETH private WETH = IWETH(0xd0A1E359811322d97991E03f863a0C30C2cF029C);
    ISoloMargin private soloMargin = ISoloMargin(0x4EC3570cADaAEE08Ae384779B0f3A45EF85289DE);

I've deposited some WETH on my contract to repay the loan, the callbackfunction is unmodified. But.. my transactions keep failing, see https://kovan.etherscan.io/tx/0xf82df4461d4b0b56c3405a2eeb8c4fbdd5abbbe0526fe144ab2e7974630ce4e4#internal for an example.

Any idea how I can troubleshoot this ? Thanks!

@cdgmachado0
Copy link

cdgmachado0 commented Jul 16, 2021

dydx's solo contract is not on any testnet @Oliver917

@devdomsos
Copy link

devdomsos commented Aug 29, 2021

You can try forking eth mainnet state with ganache-cli and then run the flashloan locally.

@cchighman
Copy link

cchighman commented Feb 10, 2022

dydx's solo contract is deployed on Kovan, for about 3 years now.

https://github.com/dydxprotocol/solo/blame/master/migrations/deployed.json

{
    "SoloMargin": {
        "1": {
            "links": {
                "AdminImpl": "0x8a6629fEba4196E0A61B8E8C94D4905e525bc055",
                "OperationImpl": "0x56E7d4520ABFECf10b38368b00723d9BD3c21ee1"
            },
            "address": "0x1E0447b19BB6EcFdAe1e4AE1694b0C3659614e4e",
            "transactionHash": "0x5d824b179e39313f45da503b1d75c1c7ce5287646a45ebe42431d50141fd451a"
        },
        "42": {
            "links": {
                "AdminImpl": "0x078D400C1a723b792de8afE240bf039b7069ac39",
                "OperationImpl": "0xcfa0451D8D1F08504Fa44a6f72D00F455b858a1d"
            },
            "address": "0x4EC3570cADaAEE08Ae384779B0f3A45EF85289DE",
            "transactionHash": "0xb949f10eabe6b7e66c7304a3e9d300375638e82932c5c1e723da3530ace277e3"
        }
    },

For correct Kovan addresses:

https://github.com/dydxprotocol/solo/blob/aeac02862d079d0bc5a984f56d2b9942c9ce41b9/migrations/3_setup.js

 if (isKovan(network)) {
    return [
      { address: '0xd0a1e359811322d97991e03f863a0c30c2cf029c' }, // Kovan WETH
      { address: '0xc4375b7de8af5a38a93548eb8453a498222c4ff2' }, // Kovan DAI
      { address: '0x03226d9241875DbFBfE0e814ADF54151e4F3fd4B' }, // Kovan USDC
    ];
  }

@tyler904
Copy link

tyler904 commented Jun 6, 2022

FlashLoan.sol:167:27: Warning: Unused function parameter. Remove or comment out the variable name to silence this warning.
function callFunction(address sender, Account.Info memory accountInfo, bytes memory data) external override {
^------------^
FlashLoan.sol:167:43: Warning: Unused function parameter. Remove or comment out the variable name to silence this warning.
function callFunction(address sender, Account.Info memory accountInfo, bytes memory data) external override {
^-----------------------------^

help pls

@ansmushtaq669
Copy link

ansmushtaq669 commented Jun 11, 2022

@tyler904 same problem! someone to guide please

@OumArEtH
Copy link

OumArEtH commented Jun 15, 2022

FlashLoan.sol:167:27: Warning: Unused function parameter. Remove or comment out the variable name to silence this warning. function callFunction(address sender, Account.Info memory accountInfo, bytes memory data) external override { ^------------^ FlashLoan.sol:167:43: Warning: Unused function parameter. Remove or comment out the variable name to silence this warning. function callFunction(address sender, Account.Info memory accountInfo, bytes memory data) external override { ^-----------------------------^

help pls

Hi, just remove the variable name and leave the type. The warning should disappear.
Cheers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment