mirror of
https://github.com/Instadapp/smart-contract.git
synced 2024-07-29 22:08:07 +00:00
dydx import fixed bugs
This commit is contained in:
parent
5cb23d4ea4
commit
1ec9051cc1
|
|
@ -21,7 +21,7 @@ contract SoloMarginContract {
|
||||||
|
|
||||||
enum ActionType {
|
enum ActionType {
|
||||||
Deposit, // supply tokens
|
Deposit, // supply tokens
|
||||||
Withdraw, // borrow tokens
|
Withdraw, // supply tokens
|
||||||
Transfer, // transfer balance between accounts
|
Transfer, // transfer balance between accounts
|
||||||
Buy, // buy an amount of some token (externally)
|
Buy, // buy an amount of some token (externally)
|
||||||
Sell, // sell an amount of some token (externally)
|
Sell, // sell an amount of some token (externally)
|
||||||
|
|
@ -65,10 +65,9 @@ contract SoloMarginContract {
|
||||||
}
|
}
|
||||||
|
|
||||||
function operate(Info[] memory accounts, ActionArgs[] memory actions) public;
|
function operate(Info[] memory accounts, ActionArgs[] memory actions) public;
|
||||||
function getAccountWei(Info memory account, uint256 marketId) public returns (Wei memory);
|
function getAccountWei(Info memory account, uint256 marketId) public view returns (Wei memory);
|
||||||
function getNumMarkets() public view returns (uint256);
|
function getNumMarkets() public view returns (uint256);
|
||||||
function getMarketTokenAddress(uint256 marketI) public view returns (address);
|
function getMarketTokenAddress(uint256 marketI) public view returns (address);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PoolInterface {
|
interface PoolInterface {
|
||||||
|
|
@ -148,7 +147,14 @@ contract Helpers is DSMath {
|
||||||
/**
|
/**
|
||||||
* @dev getting actions arg
|
* @dev getting actions arg
|
||||||
*/
|
*/
|
||||||
function getActionsArgs(address tknAccount, uint256 marketId, uint256 tokenAmt, bool sign) internal view returns (SoloMarginContract.ActionArgs[] memory) {
|
function getActionsArgs(
|
||||||
|
address tknAccount,
|
||||||
|
uint accountIndex,
|
||||||
|
uint256 marketId,
|
||||||
|
uint256 tokenAmt,
|
||||||
|
bool sign
|
||||||
|
) internal pure returns (SoloMarginContract.ActionArgs[] memory)
|
||||||
|
{
|
||||||
SoloMarginContract.ActionArgs[] memory actions = new SoloMarginContract.ActionArgs[](1);
|
SoloMarginContract.ActionArgs[] memory actions = new SoloMarginContract.ActionArgs[](1);
|
||||||
SoloMarginContract.AssetAmount memory amount = SoloMarginContract.AssetAmount(
|
SoloMarginContract.AssetAmount memory amount = SoloMarginContract.AssetAmount(
|
||||||
sign,
|
sign,
|
||||||
|
|
@ -161,7 +167,7 @@ contract Helpers is DSMath {
|
||||||
SoloMarginContract.ActionType action = sign ? SoloMarginContract.ActionType.Deposit : SoloMarginContract.ActionType.Withdraw;
|
SoloMarginContract.ActionType action = sign ? SoloMarginContract.ActionType.Deposit : SoloMarginContract.ActionType.Withdraw;
|
||||||
actions[0] = SoloMarginContract.ActionArgs(
|
actions[0] = SoloMarginContract.ActionArgs(
|
||||||
action,
|
action,
|
||||||
0,
|
accountIndex,
|
||||||
amount,
|
amount,
|
||||||
marketId,
|
marketId,
|
||||||
0,
|
0,
|
||||||
|
|
@ -175,7 +181,7 @@ contract Helpers is DSMath {
|
||||||
/**
|
/**
|
||||||
* @dev getting acccount arg
|
* @dev getting acccount arg
|
||||||
*/
|
*/
|
||||||
function getAccountArgs(address owner, uint accountId) internal view returns (SoloMarginContract.Info[] memory) {
|
function getAccountArgs(address owner, uint accountId) internal pure returns (SoloMarginContract.Info[] memory) {
|
||||||
SoloMarginContract.Info[] memory accounts = new SoloMarginContract.Info[](1);
|
SoloMarginContract.Info[] memory accounts = new SoloMarginContract.Info[](1);
|
||||||
accounts[0] = (SoloMarginContract.Info(owner, accountId));
|
accounts[0] = (SoloMarginContract.Info(owner, accountId));
|
||||||
return accounts;
|
return accounts;
|
||||||
|
|
@ -184,7 +190,7 @@ contract Helpers is DSMath {
|
||||||
/**
|
/**
|
||||||
* @dev getting dydx balance
|
* @dev getting dydx balance
|
||||||
*/
|
*/
|
||||||
function getDydxBal(address owner, uint256 marketId, uint accountId) internal returns (uint tokenBal, bool tokenSign) {
|
function getDydxBal(address owner, uint256 marketId, uint accountId) internal view returns (uint tokenBal, bool tokenSign) {
|
||||||
SoloMarginContract solo = SoloMarginContract(getSoloAddress());
|
SoloMarginContract solo = SoloMarginContract(getSoloAddress());
|
||||||
SoloMarginContract.Wei memory tokenWeiBal = solo.getAccountWei(getAccountArgs(owner, accountId)[0], marketId);
|
SoloMarginContract.Wei memory tokenWeiBal = solo.getAccountWei(getAccountArgs(owner, accountId)[0], marketId);
|
||||||
tokenBal = tokenWeiBal.value;
|
tokenBal = tokenWeiBal.value;
|
||||||
|
|
@ -194,88 +200,7 @@ contract Helpers is DSMath {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
contract DydxResolver is Helpers {
|
contract ImportHelper is Helpers {
|
||||||
|
|
||||||
event LogDeposit(address erc20Addr, uint tokenAmt, address owner);
|
|
||||||
event LogWithdraw(address erc20Addr, uint tokenAmt, address owner);
|
|
||||||
event LogBorrow(address erc20Addr, uint tokenAmt, address owner);
|
|
||||||
event LogPayback(address erc20Addr, uint tokenAmt, address owner);
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * @dev Deposit ETH/ERC20
|
|
||||||
// */
|
|
||||||
// function deposit(uint256 marketId, address erc20Addr, uint256 tokenAmt) public payable {
|
|
||||||
// if (erc20Addr == getAddressETH()) {
|
|
||||||
// ERC20Interface(getAddressWETH()).deposit.value(msg.value)();
|
|
||||||
// setApproval(getAddressWETH(), tokenAmt, getSoloAddress());
|
|
||||||
// } else {
|
|
||||||
// require(ERC20Interface(erc20Addr).transferFrom(msg.sender, address(this), tokenAmt), "Allowance or not enough bal");
|
|
||||||
// setApproval(erc20Addr, tokenAmt, getSoloAddress());
|
|
||||||
// }
|
|
||||||
// SoloMarginContract(getSoloAddress()).operate(getAccountArgs(), getActionsArgs(marketId, tokenAmt, true));
|
|
||||||
// emit LogDeposit(erc20Addr, tokenAmt, address(this));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * @dev Payback ETH/ERC20
|
|
||||||
// */
|
|
||||||
// function payback(uint256 marketId, address erc20Addr, uint256 tokenAmt) public payable {
|
|
||||||
// (uint toPayback, bool tokenSign) = getDydxBal(marketId);
|
|
||||||
// require(!tokenSign, "No debt to payback");
|
|
||||||
// toPayback = toPayback > tokenAmt ? tokenAmt : toPayback;
|
|
||||||
// if (erc20Addr == getAddressETH()) {
|
|
||||||
// ERC20Interface(getAddressWETH()).deposit.value(toPayback)();
|
|
||||||
// setApproval(getAddressWETH(), toPayback, getSoloAddress());
|
|
||||||
// msg.sender.transfer(address(this).balance);
|
|
||||||
// } else {
|
|
||||||
// require(ERC20Interface(erc20Addr).transferFrom(msg.sender, address(this), toPayback), "Allowance or not enough bal");
|
|
||||||
// setApproval(erc20Addr, toPayback, getSoloAddress());
|
|
||||||
// }
|
|
||||||
// SoloMarginContract(getSoloAddress()).operate(getAccountArgs(), getActionsArgs(marketId, toPayback, true));
|
|
||||||
// emit LogPayback(erc20Addr, toPayback, address(this));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * @dev Withdraw ETH/ERC20
|
|
||||||
// */
|
|
||||||
// function withdraw(uint256 marketId, address erc20Addr, uint256 tokenAmt) public {
|
|
||||||
// (uint toWithdraw, bool tokenSign) = getDydxBal(marketId);
|
|
||||||
// require(tokenSign, "token not deposited");
|
|
||||||
// toWithdraw = toWithdraw > tokenAmt ? tokenAmt : toWithdraw;
|
|
||||||
// SoloMarginContract solo = SoloMarginContract(getSoloAddress());
|
|
||||||
// solo.operate(getAccountArgs(), getActionsArgs(marketId, toWithdraw, false));
|
|
||||||
// if (erc20Addr == getAddressETH()) {
|
|
||||||
// ERC20Interface wethContract = ERC20Interface(getAddressWETH());
|
|
||||||
// uint wethBal = wethContract.balanceOf(address(this));
|
|
||||||
// toWithdraw = toWithdraw < wethBal ? wethBal : toWithdraw;
|
|
||||||
// setApproval(getAddressWETH(), toWithdraw, getAddressWETH());
|
|
||||||
// ERC20Interface(getAddressWETH()).withdraw(toWithdraw);
|
|
||||||
// msg.sender.transfer(toWithdraw);
|
|
||||||
// } else {
|
|
||||||
// require(ERC20Interface(erc20Addr).transfer(msg.sender, toWithdraw), "Allowance or not enough bal");
|
|
||||||
// }
|
|
||||||
// emit LogWithdraw(erc20Addr, toWithdraw, address(this));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * @dev Borrow ETH/ERC20
|
|
||||||
// */
|
|
||||||
// function borrow(uint256 marketId, address erc20Addr, uint256 tokenAmt) public {
|
|
||||||
// SoloMarginContract(getSoloAddress()).operate(getAccountArgs(), getActionsArgs(marketId, tokenAmt, false));
|
|
||||||
// if (erc20Addr == getAddressETH()) {
|
|
||||||
// setApproval(getAddressWETH(), tokenAmt, getAddressWETH());
|
|
||||||
// ERC20Interface(getAddressWETH()).withdraw(tokenAmt);
|
|
||||||
// msg.sender.transfer(tokenAmt);
|
|
||||||
// } else {
|
|
||||||
// require(ERC20Interface(erc20Addr).transfer(msg.sender, tokenAmt), "Allowance or not enough bal");
|
|
||||||
// }
|
|
||||||
// emit LogBorrow(erc20Addr, tokenAmt, address(this));
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
contract ImportHelper is DydxResolver {
|
|
||||||
struct BorrowData {
|
struct BorrowData {
|
||||||
uint[] borrowAmt;
|
uint[] borrowAmt;
|
||||||
address[] borrowAddr;
|
address[] borrowAddr;
|
||||||
|
|
@ -290,135 +215,138 @@ contract ImportHelper is DydxResolver {
|
||||||
uint supplyCount;
|
uint supplyCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserData(uint accountId, uint toConvert) internal returns(SupplyData memory, BorrowData memory) {
|
function getTokensData(uint accountId, uint toConvert) public returns(SupplyData memory, BorrowData memory) {
|
||||||
SoloMarginContract solo = SoloMarginContract(getSoloAddress());
|
SoloMarginContract solo = SoloMarginContract(getSoloAddress());
|
||||||
uint markets = solo.getNumMarkets();
|
uint markets = solo.getNumMarkets();
|
||||||
uint[] memory supplyArr = new uint[](markets);
|
SupplyData memory supplyDataArr;
|
||||||
uint[] memory borrowArr = new uint[](markets);
|
supplyDataArr.supplyAmt = new uint[](markets);
|
||||||
uint[] memory supplyId = new uint[](markets);
|
supplyDataArr.marketId = new uint[](markets);
|
||||||
uint[] memory borrowId = new uint[](markets);
|
supplyDataArr.supplyAddr = new address[](markets);
|
||||||
address[] memory supplyAddr = new address[](markets);
|
BorrowData memory borrowDataArr;
|
||||||
address[] memory borrowAddr = new address[](markets);
|
borrowDataArr.borrowAmt = new uint[](markets);
|
||||||
|
borrowDataArr.marketId = new uint[](markets);
|
||||||
|
borrowDataArr.borrowAddr = new address[](markets);
|
||||||
uint borrowCount = 0;
|
uint borrowCount = 0;
|
||||||
uint supplyCount = 0;
|
uint supplyCount = 0;
|
||||||
|
|
||||||
for (uint i = 0; i < markets; i++) {
|
for (uint i = 0; i < markets; i++) {
|
||||||
(uint tokenbal, bool tokenSign) = getDydxBal(msg.sender, i, accountId);
|
(uint tokenbal, bool tokenSign) = getDydxBal(msg.sender, i, accountId);
|
||||||
if (tokenSign) {
|
if (tokenbal > 0) {
|
||||||
supplyArr[supplyCount] = wmul(tokenbal, toConvert);
|
if (tokenSign) {
|
||||||
supplyAddr[supplyCount] = solo.getMarketTokenAddress(i);
|
supplyDataArr.supplyAmt[supplyCount] = wmul(tokenbal, toConvert);
|
||||||
supplyId[supplyCount] = i;
|
supplyDataArr.supplyAddr[supplyCount] = solo.getMarketTokenAddress(i);
|
||||||
supplyCount++;
|
supplyDataArr.marketId[supplyCount] = i;
|
||||||
} else {
|
supplyCount++;
|
||||||
borrowArr[borrowCount] = wmul(tokenbal, toConvert);
|
} else {
|
||||||
borrowAddr[borrowCount] = solo.getMarketTokenAddress(i);
|
borrowDataArr.borrowAmt[borrowCount] = wmul(tokenbal, toConvert);
|
||||||
borrowId[borrowCount] = i;
|
borrowDataArr.borrowAddr[borrowCount] = solo.getMarketTokenAddress(i);
|
||||||
borrowCount++;
|
borrowDataArr.marketId[borrowCount] = i;
|
||||||
|
borrowCount++;
|
||||||
|
}
|
||||||
|
setApproval(solo.getMarketTokenAddress(i), uint(-1), getSoloAddress());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
borrowDataArr.borrowCount = borrowCount;
|
||||||
SupplyData(
|
supplyDataArr.supplyCount = supplyCount;
|
||||||
supplyArr,
|
return (supplyDataArr, borrowDataArr);
|
||||||
supplyAddr,
|
|
||||||
supplyId,
|
|
||||||
supplyCount),
|
|
||||||
BorrowData(
|
|
||||||
borrowArr,
|
|
||||||
borrowAddr,
|
|
||||||
borrowId,
|
|
||||||
borrowCount
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ImportStruct {
|
function getOperatorActionsArgs(SupplyData memory suppyArr, BorrowData memory borrowArr) public view
|
||||||
SoloMarginContract.Info[] accounts;
|
returns(SoloMarginContract.ActionArgs[] memory)
|
||||||
SoloMarginContract.ActionArgs[] actions;
|
{
|
||||||
}
|
uint borrowCount = borrowArr.borrowCount;
|
||||||
|
uint supplyCount = suppyArr.supplyCount;
|
||||||
function createArgs(
|
uint totalCount = borrowCount + supplyCount;
|
||||||
SupplyData memory suppyArr,
|
SoloMarginContract.ActionArgs[] memory actions = new SoloMarginContract.ActionArgs[](totalCount*2);
|
||||||
BorrowData memory borrowArr
|
|
||||||
) internal view returns(SoloMarginContract.Info[] memory, SoloMarginContract.ActionArgs[] memory) {
|
|
||||||
uint borrowCount = borrowArr.borrowAmt.length;
|
|
||||||
uint supplyCount = suppyArr.supplyAmt.length;
|
|
||||||
uint totalCount = borrowArr.borrowAmt.length + suppyArr.supplyAmt.length;
|
|
||||||
SoloMarginContract.ActionArgs[] memory actions = new SoloMarginContract.ActionArgs[](totalCount);
|
|
||||||
SoloMarginContract.Info[] memory accounts = new SoloMarginContract.Info[](totalCount);
|
|
||||||
|
|
||||||
for (uint i = 0; i < borrowCount; i++) {
|
for (uint i = 0; i < borrowCount; i++) {
|
||||||
actions[i] = getActionsArgs(
|
actions[i] = getActionsArgs(
|
||||||
address(this),
|
address(this),
|
||||||
|
0,
|
||||||
borrowArr.marketId[i],
|
borrowArr.marketId[i],
|
||||||
borrowArr.borrowAmt[i],
|
borrowArr.borrowAmt[i],
|
||||||
true)[0]; // add markets , account to take token.
|
true
|
||||||
accounts[i] = getAccountArgs(msg.sender, 0)[0];
|
)[0];
|
||||||
actions[i + supplyCount + supplyCount] = getActionsArgs(
|
actions[i + totalCount + supplyCount] = getActionsArgs(
|
||||||
msg.sender,
|
getPoolAddress(), // After borrowing transfer directly to InstaDApp's Pool.
|
||||||
|
1,
|
||||||
borrowArr.marketId[i],
|
borrowArr.marketId[i],
|
||||||
borrowArr.borrowAmt[i],
|
borrowArr.borrowAmt[i],
|
||||||
false)[0];
|
false
|
||||||
accounts[i + supplyCount + supplyCount] = getAccountArgs(address(this), 0)[0];
|
)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (uint i = 0; i < supplyCount; i++) {
|
for (uint i = 0; i < supplyCount; i++) {
|
||||||
uint baseIndex = borrowCount + i;
|
uint baseIndex = borrowCount + i;
|
||||||
actions[baseIndex] = getActionsArgs(
|
actions[baseIndex] = getActionsArgs(
|
||||||
address(this),
|
address(this),
|
||||||
|
0,
|
||||||
suppyArr.marketId[i],
|
suppyArr.marketId[i],
|
||||||
suppyArr.supplyAmt[i],
|
suppyArr.supplyAmt[i],
|
||||||
false)[0];
|
false
|
||||||
accounts[i] = getAccountArgs(msg.sender, 0)[0];
|
)[0];
|
||||||
actions[baseIndex + supplyCount] = getActionsArgs(
|
actions[baseIndex + supplyCount] = getActionsArgs(
|
||||||
address(this),
|
address(this),
|
||||||
|
1,
|
||||||
suppyArr.marketId[i],
|
suppyArr.marketId[i],
|
||||||
suppyArr.supplyAmt[i],
|
suppyArr.supplyAmt[i],
|
||||||
true)[0];
|
true
|
||||||
accounts[baseIndex + supplyCount] = getAccountArgs(address(this), 0)[0];
|
)[0];
|
||||||
}
|
}
|
||||||
|
return (actions);
|
||||||
|
}
|
||||||
|
|
||||||
return (accounts, actions);
|
function getOperatorAccountArgs(uint accountId) public view returns (SoloMarginContract.Info[] memory) {
|
||||||
|
SoloMarginContract.Info[] memory accounts = new SoloMarginContract.Info[](2);
|
||||||
|
accounts[0] = getAccountArgs(msg.sender, accountId)[0];
|
||||||
|
accounts[1] = getAccountArgs(address(this), 0)[0];
|
||||||
|
return accounts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
contract ImportResolver is ImportHelper {
|
contract ImportResolver is ImportHelper {
|
||||||
event LogDydxImport(address owner, uint percentage, bool isCompound, address[] markets, address[] borrowAddr, uint[] borrowAmt);
|
event LogDydxImport(address owner, uint accountId, uint percentage, bool isCompound, SupplyData supplyData, BorrowData borrowData);
|
||||||
|
|
||||||
function importAssets(uint toConvert, uint accountId, bool isCompound) external {
|
function importAssets(
|
||||||
|
uint toConvert,
|
||||||
|
uint accountId,
|
||||||
|
bool isCompound
|
||||||
|
) external
|
||||||
|
{
|
||||||
// subtracting 0.00000001 ETH from initialPoolBal to solve Compound 8 decimal CETH error.
|
// subtracting 0.00000001 ETH from initialPoolBal to solve Compound 8 decimal CETH error.
|
||||||
uint initialPoolBal = sub(getPoolAddress().balance, 10000000000);
|
uint initialPoolBal = sub(getPoolAddress().balance, 10000000000);
|
||||||
(SupplyData memory supplyArr, BorrowData memory borrowArr) = getUserData(accountId, toConvert);
|
(SupplyData memory supplyArr, BorrowData memory borrowArr) = getTokensData(accountId, toConvert);
|
||||||
|
|
||||||
// Get liquidity assets to payback user wallet borrowed assets
|
// Get liquidity assets to payback user wallet borrowed assets
|
||||||
PoolInterface(getPoolAddress()).accessToken(borrowArr.borrowAddr, borrowArr.borrowAmt, isCompound);
|
if (borrowArr.borrowCount > 0) {
|
||||||
|
PoolInterface(getPoolAddress()).accessToken(borrowArr.borrowAddr, borrowArr.borrowAmt, isCompound);
|
||||||
SoloMarginContract solo = SoloMarginContract(getSoloAddress());
|
|
||||||
(SoloMarginContract.Info[] memory accounts, SoloMarginContract.ActionArgs[] memory actions) = createArgs(supplyArr, borrowArr);
|
|
||||||
solo.operate(accounts, actions);
|
|
||||||
// // payback user wallet borrowed assets
|
|
||||||
for (uint i = 0; i < borrowArr.borrowCount; i++) {
|
|
||||||
address erc20 = borrowArr.borrowAddr[i];
|
|
||||||
uint toPayback = borrowArr.borrowAmt[i];
|
|
||||||
if (erc20 == getAddressWETH()) {
|
|
||||||
getPoolAddress().transfer(toPayback);
|
|
||||||
} else {
|
|
||||||
require(ERC20Interface(erc20).transfer(getPoolAddress(), toPayback), "Not-enough-amt");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Creating actions args for solo operate
|
||||||
|
SoloMarginContract.ActionArgs[] memory actions = getOperatorActionsArgs(supplyArr, borrowArr);
|
||||||
|
SoloMarginContract.Info[] memory accounts = getOperatorAccountArgs(accountId);
|
||||||
|
|
||||||
|
// Import Assests from msg.sender to address(this)
|
||||||
|
SoloMarginContract solo = SoloMarginContract(getSoloAddress());
|
||||||
|
solo.operate(accounts, actions);
|
||||||
|
|
||||||
//payback InstaDApp liquidity
|
//payback InstaDApp liquidity
|
||||||
PoolInterface(getPoolAddress()).paybackToken(borrowArr.borrowAddr, isCompound);
|
if (borrowArr.borrowCount > 0) {
|
||||||
|
PoolInterface(getPoolAddress()).paybackToken(borrowArr.borrowAddr, isCompound);
|
||||||
|
}
|
||||||
|
|
||||||
uint finalPoolBal = getPoolAddress().balance;
|
uint finalPoolBal = getPoolAddress().balance;
|
||||||
assert(finalPoolBal >= initialPoolBal);
|
assert(finalPoolBal >= initialPoolBal);
|
||||||
|
|
||||||
// emit LogCompoundImport(
|
emit LogDydxImport(
|
||||||
// msg.sender,
|
msg.sender,
|
||||||
// toConvert,
|
accountId,
|
||||||
// isCompound,
|
toConvert,
|
||||||
// markets,
|
isCompound,
|
||||||
// borrowAddr,
|
supplyArr,
|
||||||
// borrowAmt
|
borrowArr
|
||||||
// );
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user