title | description | parent | tags | slug | contentType | root | |||
---|---|---|---|---|---|---|---|---|---|
Working with DSProxy |
Learn how DSProxy is used in Maker and apply it in your applications |
devtools |
|
working-with-ds-proxy |
guides |
false |
Level: Advanced
Estimated Time: 90 - 120 minutes
- Working with DSProxy
Whether you are a Keeper looking to integrate the Maker Protocol with a new source of liquidity, or an interface developer looking to cut down the number of transactions an end user has to sign, you can now implement your ideas by creating simple scripts that can atomically perform transactions across multiple contracts through DSProxy.
The Maker Protocol's approach to modularizing smart contracts and splitting logic into numerous tiny functions are great for security, but interface developers and end users interacting with them have to execute multiple transactions now to achieve a single goal. Instead of imposing the design constraints of good end-user ergonomics on the core smart contracts, it is moved to an additional compositional layer of smart contracts built with DSProxy and stateless scripts.
Keeping this functionality in a separate layer also allows developers to add additional scripts over time when new user needs emerge and better methods to compose new protocols are developed.
Understanding the DSProxy design pattern will help you quickly develop scripts that compose functionality of existing smart contracts in novel ways. Developing core smart contracts with this pattern in mind can increase their security without sacrificing usability, while reducing overall complexity, and preserving atomicity of transactions when simultaneously interacting with multiple smart contracts.
In this guide you will,
- Understand how DSProxy and scripts work through examples
- Understand the features of a DSProxy contract
- Build and deploy a new script
- Look at best practices of developing a script
- Additional details to help with deploying a script to production
- Understanding of the functions used to interact with Vaults
- Solidity development experience
Opening a Vault to generate Dai is a common action performed by users within the Maker Platform and they perform multiple transactions on the WETH, EthJoin, Vat, and DaiJoin contracts to complete it.
Transactions to execute on the WETH token contract are,
- Convert ETH to WETH using the
mint
function. - Approve the EthJoin contract to spend the user's WETH balance using the
approve
function.
Transactions to execute on the EthJoin contract are,
- Allocate the WETH to the vault using the
join
function.
Transactions to execute on the Vat contract are,
- Open the vault using the
open
function. - Lock the WETH into the vault using the
frob
function. - Draw Dai from the vault using the
frob
function. move
the Dai out of the vault.approve
the DaiJoin contract to access the user's Dai balance
Transactions to execute on DaiJoin
- Mint ERC20 Dai using the
exit
function, which in turns call themint
function of the ERC20 Dai contract.
Oasis Borrow uses a script to improve the user experience by executing the above steps atomically within a single transaction.
A relatively common task for vault owner is to reduce its debt by selling collateral. This would normally involve multiple steps that could be done in a single atomic transaction:
- Draw collateral from the vault
- Sell the collateral for Dai
- Pay back the vault debt
A user first deploys their own personal DSProxy contract and then uses it to call various scripts for the goals they wish to achieve. This DSProxy contract can also directly own digital assets long term since the user always has full ownership of the contract and it can be treated as an extension of the user's own ethereum address.
Scripts are implemented in Solidity as functions and multiple scripts are typically combined and deployed together as a single contract. A DSProxy contract can only execute one script in a single transaction. This section will focus on the features of a DSProxy contract and look at how scripts work in the next section.
Ownership of a DSProxy contract is set to an address when it is deployed. There is support for authorities based on DSAuth if there is a need for ownership of the DSProxy contract to be shared among multiple users.
execute(address target, bytes data)
function implements the core functionality of DSProxy. It takes in two inputs, an address
of the contract containing scripts, and data
which contains calldata to identify the script that needs to be executed along with it's input data.
msg.sender
when the script is being executed will continue to be the user address instead of the address of the DSProxy contract.
execute(bytes code, bytes data)
is an additional function that can be used when a user wants to deploy a contract containing scripts and then call one of the scripts in a single transaction. A cache
registers the address of contract deployed to save gas by skipping deployment when other users call execute
with the same bytecode later.
A DSProxy contract generates a event called LogNote
with these values indexed when execute()
is called,
- Function signature,
0x1cff79cd
- Owner of the DSProxy contract,
msg.sender
- Contract address which contains the script,
address
- Calldata which contains function signature of script being executed and its input data,
data
The function build
in the DSProxyFactory contract is used to deploy a personal DSProxy contract. Since proxy addresses are derived from the internal nonce of the DSProxyFactory, it's recommended a 20 block confirmation time follows the build
transaction, lest an accidental address re-assignment during a block re-org. For production use cases on mainnet you can use a common factory contract that is already being used by existing projects to avoid deploying redundant DSProxy contracts for users who already have one. Please check the Production Usage section in this guide for more details.
You have seen an example earlier of how a script can help Vault owners to reduce their debt by selling collateral. Oasis exchange contracts are a good source of liquidity especially for buying small amounts of collateral. In this section, you will create a script that will allow users to draw Eth from their vault, sell it on Oasis and wipe debt from a Vault.
You'll use dapp
and seth
while working through this section but you can also use your own tool of choice like the Remix IDE to execute these steps. Instructions to install both the tools can be found here.
You have to create a ~/.sethrc
file and configure it with these values to work with the Kovan testnet,
export SETH_CHAIN=kovan
export ETH_FROM=0xYourKovanAddressFromKeyStoreOrLedger
export ETH_GAS=4000000
export ETH_GAS_PRICE=2500000000
It is usually recommended to configure ETH_RPC_URL
to point an Infura endpoint or your own Kovan Ethereum node.
Create a new folder and open it
mkdir delev && cd delev
Initialize a dapp project within it
dapp init
First you need to add the required interfaces to interact with functions on those contracts later in the script.
This contract will utilize the ERC20 contracts for WETH and DAI (GemLike
), their adaptor contracts (DaiJoinLike
and GemJoinLike
), interact with the vat
(VatLike
), Oasis' MatchingMarket (OasisLike
) and the CDPManager
(ManagerLike
):
interface GemLike {
function approve(address, uint) external;
function transfer(address, uint) external;
function transferFrom(address, address, uint) external;
function deposit() external payable;
function withdraw(uint) external;
}
interface DaiJoinLike {
function vat() external returns (VatLike);
function dai() external returns (GemLike);
function join(address, uint) external payable;
function exit(address, uint) external;
}
interface VatLike {
function can(address, address) external view returns (uint);
function ilks(bytes32) external view returns (uint, uint, uint, uint, uint);
function dai(address) external view returns (uint);
function urns(bytes32, address) external view returns (uint, uint);
function frob(bytes32, address, address, address, int, int) external;
function hope(address) external;
function move(address, address, uint) external;
}
interface GemJoinLike {
function dec() external returns (uint);
function gem() external returns (GemLike);
function join(address, uint) external payable;
function exit(address, uint) external;
}
interface OasisLike {
function sellAllAmount(address pay_gem, uint pay_amt, address buy_gem, uint min_fill_amount) external returns (uint);
}
interface ManagerLike {
function cdpCan(address, uint, address) external view returns (uint);
function ilks(uint) external view returns (bytes32);
function owns(uint) external view returns (address);
function urns(uint) external view returns (address);
function vat() external view returns (address);
function open(bytes32) external returns (uint);
function give(uint, address) external;
function cdpAllow(uint, address, uint) external;
function urnAllow(address, uint) external;
function frob(uint, int, int) external;
function flux(uint, address, uint) external;
function move(uint, address, uint) external;
function exit(address, uint, address, uint) external;
function quit(uint, address) external;
function enter(address, uint) external;
function shift(uint, uint) external;
}
The vat
records the individual vault debt balances by dividing the Dai amounts by the accrued rate
for that ilk type. This facilitates the calculation of vault fees (For more details about rate accumulation, read this guide). Here, we adapt a function from dss-proxy-actions
function _getWipeDart(
address vat,
uint dai,
address urn,
bytes32 ilk
) internal view returns (int dart) {
// Gets actual rate from the vat
(, uint rate,,,) = VatLike(vat).ilks(ilk);
// Gets actual art value of the urn
(, uint art) = VatLike(vat).urns(ilk, urn);
// Uses the whole dai balance in the vat to reduce the debt
dart = int(dai / rate);
// Checks the calculated dart is not higher than urn.art (total debt), otherwise uses its value
dart = uint(dart) <= art ? - dart : - int(art);
}
Add a new function wipeWithEth
which takes in the following inputs,
- Address of the CDP Manager contract
- Address of the MCD ETH Adapter (ethJoin)
- Address of the MCD DAI Adapter (daiJoin)
- Address of the current Oasis Matching Market contract
- Id of the CDP in decimals. Ex: 44
- Amount of Eth to be used
function wipeWithEth(
address manager,
address ethJoin,
address daiJoin,
address oasisMatchingMarket,
uint cdp,
uint wadEth
)
public
{
// logic
}
Within the function body, ensure at least some Eth is being removed from the vault, using a require statement:
require(wadEth > 0);
Then you determine what is the urn
address for our vault:
address urn = ManagerLike(manager).urns(cdp);
First real step is withdraw the Ether from the vault. This is done by the frob
function on the CDP Manager. After this is done, you need to move it from the urn
address to our proxy and converting the internal WETH balance to an actual ERC20.
//Remove the WETH from the vault
ManagerLike(manager).frob(cdp, -int(wadEth), int(0));
// Moves the WETH from the CDP urn to proxy's address
ManagerLike(manager).flux(cdp, address(this), wadEth);
// Exits WETH amount to proxy address as a token
GemJoinLike(ethJoin).exit(address(this), wadEth);
At this step, you have withdrawn the Ether from the vault. If remove that ether makes the vault undercollaterized, the transaction will fail here and revert.
Oasis has a sellAllAmount
method that market sells a ERC20 for another ERC20 token, here Weth and Dai. For this to work, make sure that the Kovan Oasis Trade has the required open orders.
//Approve Oasis to obtain the WETH to be sold
GemJoinLike(ethJoin).gem().approve(oasisMatchingMarket,wadEth);
//Market order to sell the WETH for DAI
uint daiAmt = OasisLike(oasisMatchingMarket).sellAllAmount(
address(GemJoinLike(ethJoin).gem()),
wadEth,
address(DaiJoinLike(daiJoin).dai()),
uint(0)
);
In this naive implementation, you are market selling the Ether for Dai, irrespective of the on-chain price compared to the market. It could be possible to query an oracle to make sure there is no slippage or have the user specify a minimum amount of Dai to be received.
You now wipe debt of the Vault with the Dai that you just acquired. You have to first approve
the Dai Adapter to take our Dai and have it move it into the urn
(using join
):
// Approves adapter to take the DAI amount
DaiJoinLike(daiJoin).dai().approve(daiJoin, daiAmt);
// Joins DAI into the vat
DaiJoinLike(daiJoin).join(urn, daiAmt);
To finally wipe the Dai, you have to calculate its art value in accordance to the current rate (so it takes into account fees) and finally wipe the debt using frob
:
// Calculate the amount of art corresponding to DAI (accumulated rates)
int dart = _getWipeDart(ManagerLike(manager).vat(), VatLike(ManagerLike(manager).vat()).dai(urn), urn, ManagerLike(manager).ilks(cdp));
// Pay back the art/dai in the vault
ManagerLike(manager).frob(cdp, int(0), dart);
Before you proceed to the next section of this guide, please ensure your code matches the Delev
contract below
contract Delev {
function wipeWithEth(
address manager,
address ethJoin,
address daiJoin,
address oasisMatchingMarket,
uint cdp,
uint wadEth
) public {
address urn = ManagerLike(manager).urns(cdp);
require(wadEth > 0);
//Remove the WETH from the vault
ManagerLike(manager).frob(cdp, -int(wadEth), int(0));
// Moves the WETH from the CDP urn to proxy's address
ManagerLike(manager).flux(cdp, address(this), wadEth);
// Exits WETH amount to proxy address as a token
GemJoinLike(ethJoin).exit(address(this), wadEth);
//Approve Oasis to obtain the WETH to be sold
GemJoinLike(ethJoin).gem().approve(oasisMatchingMarket,wadEth);
//Market order to sell the WETH for DAI
uint daiAmt = OasisLike(oasisMatchingMarket).sellAllAmount(
address(GemJoinLike(ethJoin).gem()),
wadEth,
address(DaiJoinLike(daiJoin).dai()),
uint(0)
);
// Approves adapter to take the DAI amount
DaiJoinLike(daiJoin).dai().approve(daiJoin, daiAmt);
// Joins DAI into the vat
DaiJoinLike(daiJoin).join(urn, daiAmt);
// Calculate the amount of art corresponding to DAI (accumulated rates)
int dart = _getWipeDart(ManagerLike(manager).vat(), VatLike(ManagerLike(manager).vat()).dai(urn), urn, ManagerLike(manager).ilks(cdp));
// Pay back the art/dai in the vault
ManagerLike(manager).frob(cdp, int(0), dart);
}
function _getWipeDart(
address vat,
uint dai,
address urn,
bytes32 ilk
) internal view returns (int dart) {
// Gets actual rate from the vat
(, uint rate,,,) = VatLike(vat).ilks(ilk);
// Gets actual art value of the urn
(, uint art) = VatLike(vat).urns(ilk, urn);
// Uses the whole dai balance in the vat to reduce the debt
dart = int(dai / rate);
// Checks the calculated dart is not higher than urn.art (total debt), otherwise uses its value
dart = uint(dart) <= art ? - dart : - int(art);
}
}
Before you begin, ensure you have some Kovan ETH to pay gas for transactions and Kovan Dai on the address by following instructions on this guide
Build the delev
project
dapp build --extract
Deploy the Delev contract
dapp create Delev
Make a note of the contract address returned after successful execution and store it as a variable
export DELEV=0x990f8388b5cb113e63d119d296e68590283e823e
Deploy your own DSProxy contract for your address using the factory contract present on Kovan
export PROXYREGISTRY=0x64a436ae831c1672ae81f674cab8b6775df3475c
seth send $PROXYREGISTRY 'build()'
This transaction might fail if you already have deployed a DSProxy contract before from this address. You can check if you have one now with this command
seth call $PROXYREGISTRY 'proxies(address)(address)' $ETH_FROM
Make a note of the returned DSProxy contract address and store it as a variable.
export MYPROXY=0xYourDSProxyAddress
You can prepare calldata to extract and sell 0.01 ETH from our vault #560 on Kovan using this command with the following inputs,
- Address of the CDP Manager contract
- Address of the MCD ETH Adapter (ethJoin)
- Address of the MCD DAI Adapter (daiJoin)
- Address of the current Oasis Matching Market contract
- Id of the CDP in decimals. (560)
- Amount of Eth to be used: 0.01
export CDP_MANAGER=0x1476483dD8C35F25e568113C5f70249D3976ba21
export ETH_JOIN=0x775787933e92b709f2a3C70aa87999696e74A9F8
export DAI_JOIN=0x5AA71a3ae1C0bd6ac27A1f28e1415fFFB6F15B8c
export OASIS=0xe325acB9765b02b8b418199bf9650972299235F4
CALLDATA=$(seth calldata 'wipeWithEth(address,address,address,address,uint,uint)' $CDP_MANAGER $ETH_JOIN $DAI_JOIN $OASIS $(seth --to-hexdata $(seth --to-uint256 560)) $(seth --to-uint256 $(seth --to-wei 0.01 eth)))
CALLDATA should look like this:
echo $CALLDATA
0xb46858180000000000000000000000001476483dd8c35f25e568113c5f70249d3976ba21000000000000000000000000775787933e92b709f2a3c70aa87999696e74a9f80000000000000000000000005aa71a3ae1c0bd6ac27a1f28e1415fffb6f15b8c000000000000000000000000e325acb9765b02b8b418199bf9650972299235f4000000000000000000000000000000000000000000000000000000000000023000000000000000000000000000000000000000000000000000038d7ea4c68000
Call execute on the DSProxy contract with these inputs,
- Address of the deployed
Delev
contract - Calldata to execute the
wipeWithEth
script
seth send $MYPROXY 'execute(address,bytes memory)' $DELEV $CALLDATA
If the call worked correctly, you will see the Ether and Dai balances on the contract reduced by a small amount.
Use require
when using transferFrom
within the script to ensure the transaction fails when a token transfer is unsuccessful
Deploying a script to production involves creating user interfaces that can handle a DSProxy contract deployment for users who need one, and then facilitating their interactions with various deployed scripts through their deployed DSProxy contracts.
A common Proxy Registry can be used by all projects to deploy DSProxy contracts for users. The address of the deployed DSProxy contract is stored in the registry and can be looked up in the future to avoid creating a new DSProxy contract for users who already have one.
Proxy Registries are already available on these networks,
- Mainnet:
0x4678f0a6958e4d2bc4f1baf7bc52e8f3564f3fe4
- Kovan:
0x64a436ae831c1672ae81f674cab8b6775df3475c
Proxy Recover Funds interface can be used to recover and transfer ETH back to their address if it gets stuck within the DSProxy contract after a failed transaction.
Writing scripts can help you solve a variety of problems you encounter as a developer trying to improve the user experience for your users, or even as a power user interacting with ethereum protocols. The hope is this guide has covered all the relevant details to help you get started with DSProxy.
- Rocket chat - #dev channel