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

Create a guard that allows trading any token #261

Merged
merged 2 commits into from
Jan 1, 2025
Merged
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
59 changes: 46 additions & 13 deletions contracts/guard/src/GuardV0Base.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import "./IGuard.sol";
*
* - Abstract base contract to deal with different ownership modifiers and initialisers (Safe, OpenZeppelin)
*
* TODO: Externally managed safe token registry
*
*/
abstract contract GuardV0Base is IGuard {
using Path for bytes;
Expand Down Expand Up @@ -56,7 +58,7 @@ abstract contract GuardV0Base is IGuard {
uint256 amountInMaximum;
}

// Allowed ERC20.approve()
// Allowed external smart contract calls (address, function selector) tuples
mapping(address target => mapping(bytes4 selector => bool allowed)) public allowedCallSites;

// How many call sites we have enabled all-time counter.
Expand All @@ -83,6 +85,12 @@ abstract contract GuardV0Base is IGuard {
// Allowed routers
mapping(address destination => bool allowed) public allowedDelegationApprovalDestinations;

// Allow trading any token
//
// Dangerous, as malicious/compromised trade-executor can drain all assets through creating fake tokens
//
bool public anyAsset;

event CallSiteApproved(address target, bytes4 selector, string notes);
event CallSiteRemoved(address target, bytes4 selector, string notes);

Expand All @@ -104,10 +112,17 @@ abstract contract GuardV0Base is IGuard {
event AssetApproved(address sender, string notes);
event AssetRemoved(address sender, string notes);

event AnyAssetSet(bool value, string notes);

// Implementation needs to provide its own ownership policy hooks
modifier onlyGuardOwner() virtual;

// Implementation needs to provide its own ownership policy hooks
function getGovernanceAddress() virtual public view returns (address);

/**
* Calculate Solidity 4-byte function selector from a string.
*/
function getSelector(string memory _func) internal pure returns (bytes4) {
// https://solidity-by-example.org/function-selector/
return bytes4(keccak256(bytes(_func)));
Expand Down Expand Up @@ -195,6 +210,15 @@ abstract contract GuardV0Base is IGuard {

// Basic check if any target contract is whitelisted
function isAllowedCallSite(address target, bytes4 selector) public view returns (bool) {

// If we have dynamic whitelist/any token, we cannot check approve() call sites of
// individual tokens
if(anyAsset) {
if(selector == getSelector("approve(address,uint256)")) {
return true;
}
}

return allowedCallSites[target][selector];
}

Expand All @@ -219,8 +243,11 @@ abstract contract GuardV0Base is IGuard {
return allowedDelegationApprovalDestinations[receiver] == true;
}

/**
* Are we allowed to trade/own an ERC-20.
*/
function isAllowedAsset(address token) public view returns (bool) {
return allowedAssets[token] == true;
return anyAsset || allowedAssets[token] == true;
}

function validate_transfer(bytes memory callData) public view {
Expand Down Expand Up @@ -249,6 +276,23 @@ abstract contract GuardV0Base is IGuard {
allowAsset(token, notes);
}

function whitelistUniswapV3Router(address router, string calldata notes) external {
allowCallSite(router, getSelector("exactInput((bytes,address,uint256,uint256,uint256))"), notes);
allowCallSite(router, getSelector("exactOutput((bytes,address,uint256,uint256,uint256))"), notes);
allowApprovalDestination(router, notes);
}

function whitelistUniswapV2Router(address router, string calldata notes) external {
allowCallSite(router, getSelector("swapExactTokensForTokens(uint256,uint256,address[],address,uint256)"), notes);
allowApprovalDestination(router, notes);
}

// Enable unlimited trading space
function setAnyAssetAllowed(bool value, string calldata notes) external onlyGuardOwner {
anyAsset = value;
emit AnyAssetSet(value, notes);
}

// Satisfy IGuard
function validateCall(
address sender,
Expand Down Expand Up @@ -311,11 +355,6 @@ abstract contract GuardV0Base is IGuard {
}
}

function whitelistUniswapV2Router(address router, string calldata notes) external {
allowCallSite(router, getSelector("swapExactTokensForTokens(uint256,uint256,address[],address,uint256)"), notes);
allowApprovalDestination(router, notes);
}

// validate Uniswap v3 trade
function validate_exactInput(bytes memory callData) public view {
(ExactInputParams memory params) = abi.decode(callData, (ExactInputParams));
Expand Down Expand Up @@ -349,12 +388,6 @@ abstract contract GuardV0Base is IGuard {
}
}

function whitelistUniswapV3Router(address router, string calldata notes) external {
allowCallSite(router, getSelector("exactInput((bytes,address,uint256,uint256,uint256))"), notes);
allowCallSite(router, getSelector("exactOutput((bytes,address,uint256,uint256,uint256))"), notes);
allowApprovalDestination(router, notes);
}

// validate 1delta trade
function validate_1deltaMulticall(bytes memory callData) public view {
(bytes[] memory callArr) = abi.decode(callData, (bytes[]));
Expand Down
2 changes: 1 addition & 1 deletion eth_defi/abi/guard/GuardV0.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion eth_defi/abi/guard/SimpleVaultV0.json

Large diffs are not rendered by default.

74 changes: 73 additions & 1 deletion tests/guard/test_guard_simple_vault_uniswap_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,6 @@ def test_guard_token_in_not_approved(
target, call_data = encode_simple_vault_transaction(trade_call)
vault.functions.performCall(target, call_data).transact({"from": asset_manager})


def test_guard_token_in_not_approved(
uniswap_v2: UniswapV2Deployment,
weth_usdc_pair: PairDetails,
Expand Down Expand Up @@ -458,3 +457,76 @@ def test_guard_third_party_trade(
with pytest.raises(TransactionFailed, match="Sender not allowed"):
target, call_data = encode_simple_vault_transaction(trade_call)
vault.functions.performCall(target, call_data).transact({"from": third_party})


def test_guard_can_trade_any_asset_uniswap_v2(
web3: Web3,
uniswap_v2: UniswapV2Deployment,
weth_usdc_pair: PairDetails,
owner: str,
asset_manager: str,
deployer: str,
weth: Contract,
usdc: Contract,
):
"""After whitelist removed, we can trade any asset."""
weth = uniswap_v2.weth
vault = deploy_contract(web3, "guard/SimpleVaultV0.json", deployer, asset_manager)
vault.functions.initialiseOwnership(owner).transact({"from": deployer})

guard = get_deployed_contract(web3, "guard/GuardV0.json", vault.functions.guard().call())
guard.functions.whitelistUniswapV2Router(uniswap_v2.router.address, "Allow Uniswap v2").transact({"from": owner})
guard.functions.setAnyAssetAllowed(True, "Allow any asset").transact({"from": owner})

usdc_amount = 10_000 * 10**6
usdc.functions.transfer(vault.address, usdc_amount).transact({"from": deployer})

path = [usdc.address, weth.address]

#
# Buy WETH
#

approve_call = usdc.functions.approve(
uniswap_v2.router.address,
usdc_amount,
)

target, call_data = encode_simple_vault_transaction(approve_call)
vault.functions.performCall(target, call_data).transact({"from": asset_manager})

trade_call = uniswap_v2.router.functions.swapExactTokensForTokens(
usdc_amount,
0,
path,
vault.address,
FOREVER_DEADLINE,
)

target, call_data = encode_simple_vault_transaction(trade_call)
vault.functions.performCall(target, call_data).transact({"from": asset_manager})

weth_amount = 3696700037078235076
assert weth.functions.balanceOf(vault.address).call() == weth_amount

#
# Sell it back
#

approve_call = weth.functions.approve(
uniswap_v2.router.address,
weth_amount,
)
target, call_data = encode_simple_vault_transaction(approve_call)
vault.functions.performCall(target, call_data).transact({"from": asset_manager})

trade_call = uniswap_v2.router.functions.swapExactTokensForTokens(
weth_amount,
0,
[weth.address, usdc.address],
vault.address,
FOREVER_DEADLINE,
)

target, call_data = encode_simple_vault_transaction(trade_call)
vault.functions.performCall(target, call_data).transact({"from": asset_manager})