Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create tsx for cow zap transaction #734

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions packages/contracts/src/dollar/cow-minter/Minter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {IUbiquityPool} from "../interfaces/IUbiquityPool.sol";

contract Minter {
address public immutable ubiquityPool;

constructor(address ubiquityPool_) {
ubiquityPool = ubiquityPool_;
}

function getAccountAddress(address user) external view returns (address) {
return _getAccountAddress(user);
}

function getAccountBalance(
address user,
IERC20 token
) public view returns (uint256) {
return token.balanceOf(_getAccountAddress(user));
}

function ensureAccount(address user) public returns (MintAccount) {
address accountAddress = _getAccountAddress(user);
uint256 codeSize;
assembly {
codeSize := extcodesize(accountAddress)
}

if (codeSize > 0) {
return MintAccount(accountAddress);
} else {
MintAccount newAccount = new MintAccount{salt: bytes32(0)}(
user,
ubiquityPool
);
require(
accountAddress == address(newAccount),
"account does not expected deployment address"
);

return newAccount;
}
}

function mint(
address user,
address token,
uint256 amountIn,
uint256 dollarMin
) public {
ensureAccount(user).mintDollar(token, amountIn, dollarMin);
}

function mintAll(address user, address token, uint256 dollarMin) external {
mint(user, token, getAccountBalance(user, IERC20(token)), dollarMin);
}

function withdraw(address user, address token, uint256 amount) public {
ensureAccount(user).withdraw(token, amount);
}

function withdrawAll(address user, address token) external {
withdraw(user, token, getAccountBalance(user, IERC20(token)));
}

function _getAccountAddress(address user) internal view returns (address) {
return
address(
uint160(
uint256(
keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
bytes32(0),
keccak256(
abi.encodePacked(
type(MintAccount).creationCode,
abi.encode(user, ubiquityPool)
)
)
)
)
)
)
);
}
}

contract MintAccount {
address public immutable user;
address public immutable ubiquityPool;

constructor(address user_, address ubiquityPool_) {
user = user_;
ubiquityPool = ubiquityPool_;
}

function mintDollar(
address token,
uint256 amountIn,
uint256 amountOutMin
) external {
IERC20(token).approve(ubiquityPool, amountIn);
IUbiquityPool(ubiquityPool).mintDollar(token, amountIn, amountOutMin);
}

function withdraw(address token, uint256 amount) external returns (bool) {
return IERC20(token).transfer(user, amount);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ library LibUbiquityPool {
using SafeERC20 for IERC20;

bytes32 constant UBIQUITY_POOL_STORAGE_POSITION =
bytes32(uint256(keccak256("ubiquity.contracts.ubiquity.pool.storage")) - 1);
bytes32(
uint256(keccak256("ubiquity.contracts.ubiquity.pool.storage")) - 1
);

function ubiquityPoolStorage()
internal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ contract UbiquityPoolFacetTest is DiamondSetup {
StakingShare stakingShare;
BondingShare stakingShareV1;

function setUp() public override {
function setUp() public virtual override {
super.setUp();
crvToken = new MockERC20("3 CRV", "3CRV", 18);
curve3CrvToken = address(crvToken);
Expand Down
52 changes: 52 additions & 0 deletions packages/contracts/test/dollar/Minter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {Minter, MintAccount} from "../../src/dollar/cow-minter/Minter.sol";
import {UbiquityPoolFacetTest} from "../diamond/facets/UbiquityPoolFacet.t.sol";
import {MockERC20} from "../../src/dollar/mocks/MockERC20.sol";
import "forge-std/console.sol";

contract MinterTest is UbiquityPoolFacetTest {
Minter public minter;
MintAccount public mintAccount;
MockERC20 public collateral;

function setUp() public override {
super.setUp();
minter = new Minter(address(diamond));

collateral = new MockERC20("collateral", "collateral", 18);
collateral.mint(secondAccount, 10 ether);
vm.prank(admin);
IUbiquityPoolFacet.addToken(address(collateral), (metapool));
vm.prank(admin);
IUbiquityPoolFacet.setMintActive(address(collateral), true);
}

function test_createAccountIfNoneExists() public {
address accountAddress = minter.getAccountAddress(secondAccount);
uint256 codeSize;
assembly {
codeSize := extcodesize(accountAddress)
}
assertEq(codeSize, 0);

MintAccount mintAccount_ = minter.ensureAccount(secondAccount);
uint256 codeSize_;
assembly {
codeSize_ := extcodesize(mintAccount_)
}
assertGt(codeSize_, 0);
}

function test_MintFunction() public {
uint256 preBal = IDollar.balanceOf(secondAccount);
vm.startPrank(secondAccount);
mintAccount = minter.ensureAccount(secondAccount);
collateral.transfer(address(mintAccount), 5 ether);
minter.mintAll(secondAccount, address(collateral), 0);
minter.withdrawAll(secondAccount, address(IDollar));
uint256 postBal = IDollar.balanceOf(secondAccount);
assertGt(postBal, preBal);
}
}
199 changes: 199 additions & 0 deletions packages/dapp/components/ubiquity-pool/cow-zap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { ethers } from "ethers";
import useWeb3 from "../lib/hooks/use-web-3";

const { walletAddress, signer } = useWeb3();

const LUSD = new ethers.Contract(
"0x5f98805A4E8be255a32880FDeC7F6728C6568bA0",
[
"function decimals() view returns (uint8)",
"function name() view returns (string)",
"function version() view returns (string)",
"function nonces(address owner) view returns (string)",
`function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
)`,
],
signer
);

const SETTLEMENT = new ethers.Contract("0x9008D19f58AAbD9eD0D60971565AA8510560ab41", [], signer);

const VAULT_RELAYER = new ethers.Contract("0xC92E8bdf79f0507f65a392b0ab4667716BFE0110", [], signer);

const chainId = await signer?.getChainId();

async function cowZap(sellToken: string, sellAmount: number, minterAddress: string) {
const Token = new ethers.Contract(
sellToken,
[
"function decimals() view returns (uint8)",
"function name() view returns (string)",
"function version() view returns (string)",
"function nonces(address owner) view returns (string)",
`function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
)`,
],
signer
);

const MINTER = new ethers.Contract(
minterAddress,
[`function getAccountAddress(address user) view returns (address)`, `function mintAll(address user view returns (address))`],
signer
);

const permit = {
owner: walletAddress,
spender: VAULT_RELAYER.address,
value: orderConfig.sellAmount,
nonce: await Token.nonces(walletAddress),
deadline: ethers.constants.MaxUint256,
};

const permitParams = [permit.owner, permit.spender, permit.value, permit.deadline, permitSignature.v, permitSignature.r, permitSignature.s];

const permitHook = {
target: Token.address,
callData: Token.interface.encodeFunctionData("permit", permitParams),
gasLimit: `${await Token.estimateGas.permit(...permitParams)}`,
};

const orderConfig = {
sellToken: Token.address,
buyToken: LUSD.address,
sellAmount: sellAmount,
type: "sell",
partiallyFillable: false,
sellTokenBalance: "erc20",
buyTokenBalance: "erc20",
receiver: "",
appData: JSON.stringify({
backend: {
hooks: {
pre: [permitHook],
post: [bridgeHook],
},
},
}),
};

const permitSignature = ethers.utils.splitSignature(
await signer!._signTypedData(
{
name: await Token.name(),
version: await Token.version(),
chainId,
verifyingContract: Token.address,
},
{
Permit: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
{ name: "value", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
},
permit
)
);

orderConfig.receiver = await MINTER.getAccountAddress(walletAddress);
const bridgeHook = {
target: MINTER.address,
callData: MINTER.interface.encodeFunctionData("mintAll", [walletAddress, LUSD.address]),
// Approximate gas limit determined with Tenderly.
gasLimit: "228533",
};
console.log("bridge hook:", bridgeHook);

/*** Order Creation ***/

orderConfig.appData = JSON.stringify({
backend: {
hooks: {
pre: [permitHook],
post: [bridgeHook],
},
},
});
const { id: quoteId, quote } = await fetch("https://barn.api.cow.fi/mainnet/api/v1/quote", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
from: walletAddress,
sellAmountBeforeFee: orderConfig.sellAmount,
...orderConfig,
}),
}).then((response) => response.json());
console.log("quote:", quoteId, quote);

const orderData = {
...orderConfig,
sellAmount: quote.sellAmount,
buyAmount: `${ethers.BigNumber.from(quote.buyAmount).mul(99).div(100)}`,
validTo: quote.validTo,
appData: ethers.utils.id(orderConfig.appData),
feeAmount: quote.feeAmount,
};
const orderSignature = await signer._signTypedData(
{
name: "Gnosis Protocol",
version: "v2",
chainId,
verifyingContract: SETTLEMENT.address,
},
{
Order: [
{ name: "sellToken", type: "address" },
{ name: "buyToken", type: "address" },
{ name: "receiver", type: "address" },
{ name: "sellAmount", type: "uint256" },
{ name: "buyAmount", type: "uint256" },
{ name: "validTo", type: "uint32" },
{ name: "appData", type: "bytes32" },
{ name: "feeAmount", type: "uint256" },
{ name: "kind", type: "string" },
{ name: "partiallyFillable", type: "bool" },
{ name: "sellTokenBalance", type: "string" },
{ name: "buyTokenBalance", type: "string" },
],
},
orderData
);

const orderUid = await fetch("https://barn.api.cow.fi/mainnet/api/v1/orders", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
...orderData,
from: walletAddress,
appData: orderConfig.appData,
appDataHash: orderData.appData,
signingScheme: "eip712",
signature: orderSignature,
quoteId,
}),
}).then((response) => response.json());
console.log("order:", orderUid);
}

export default cowZap;
2 changes: 1 addition & 1 deletion packages/dapp/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@
"@/types/contracts": ["types/contracts"]
}
},
"include": ["global.d.ts", "next-env.d.ts", "**/*.ts", "**/*.tsx"],
"include": ["global.d.ts", "next-env.d.ts", "**/*.ts", "**/*.tsx", "components/ubiquity-pool/cow-zap.tsx"],
"exclude": ["node_modules"]
}
Loading