Skip to content

Commit

Permalink
Create a guard that allows trading any token (#261)
Browse files Browse the repository at this point in the history
- Create very dangerous "whitelist any token policy" for prototype strategies
  • Loading branch information
miohtama authored Jan 1, 2025
1 parent cb26123 commit b096e4c
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 16 deletions.
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})

0 comments on commit b096e4c

Please sign in to comment.