[BIP-442] Permissions Preset Update Request #2

Due Dilligence: BIP-442 Karpatkey Payload

Tenderly Simulation

Interpreting Events

The payload can in this case be intepreted through the events it emits on a successful (simulated) execution. This is possible because the only functions that are called are approvals on ERC20s or scope changes on the Roles module. All these functions emit relevant and exhaustive events.

Approval

Approval events are pretty straight forward to interpret:

{
    "owner": "0x0efccbb9e2c09ea29551879bd9da32362b32fc89"
    "spender": "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0"
    "value": "115792089237316195423570985008687907853269984665640564039457584007913129639935"
}

The event is emitted from 0xae7ab96520de3a18e5e111b5eaab095312d7fe84, which is stETH. On it, the managed safe (0x0efcc) approves the spender (wstETH) to spend infinite amount of tokens.

In all cases here, the owner is the managed safe and the amount of tokens is infinite. Mapping of tokens and spenders:

token (symbol) spender (label)
0xae7ab96520de3a18e5e111b5eaab095312d7fe84 (stETH) 0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0 (wstETH)
0xae7ab96520de3a18e5e111b5eaab095312d7fe84 (stETH) 0x889edc2edab5f40e902b864ad4d7ade8e412f9b1 (unstETH)
0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0 (wstETH) 0x889edc2edab5f40e902b864ad4d7ade8e412f9b1 (unstETH)
0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 (USDC) 0xc3d688b66703497daa19211eedff47f25384cdc3 (cUSDCv3)
0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 (USDC) 0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2 (Aave: Pool V3)
0x6b175474e89094c44da98b954eedeac495271d0f (DAI) 0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2 (Aave: Pool V3)
0xae78736cd615f374d3085123a210448e74fc6393 (rETH) 0x16d5a408e807db8ef7c578279beeee6b228f1c1c (RocketSwapRouter)
0x6b175474e89094c44da98b954eedeac495271d0f (DAI) 0x373238337bfe1146fb49989fc222523f83081ddb (DsrManager)
0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9 (AAVE) 0xc92e8bdf79f0507f65a392b0ab4667716bfe0110 (GPv2VaultRelayer; CoW Swap)
0xc00e94cb662c3520282e6f5717214004a7f26888 (COMP) 0xc92e8bdf79f0507f65a392b0ab4667716bfe0110 (GPv2VaultRelayer; CoW Swap)
0x6b175474e89094c44da98b954eedeac495271d0f (DAI) 0xc92e8bdf79f0507f65a392b0ab4667716bfe0110 (GPv2VaultRelayer; CoW Swap)
0xae78736cd615f374d3085123a210448e74fc6393 (RETH) 0xc92e8bdf79f0507f65a392b0ab4667716bfe0110 (GPv2VaultRelayer; CoW Swap)
0x20bc832ca081b91433ff6c17f85701b6e92486c5 (rETH2) 0xc92e8bdf79f0507f65a392b0ab4667716bfe0110 (GPv2VaultRelayer; CoW Swap)
0xfe2e637202056d30016725477c5da089ab0a043a (sETH2) 0xc92e8bdf79f0507f65a392b0ab4667716bfe0110 (GPv2VaultRelayer; CoW Swap)
0x889edc2edab5f40e902b864ad4d7ade8e412f9b1 (unstETH) 0xc92e8bdf79f0507f65a392b0ab4667716bfe0110 (GPv2VaultRelayer; CoW Swap)
0x48e5413b73add2434e47504e2a22d14940dbfe78 (INRM) 0xc92e8bdf79f0507f65a392b0ab4667716bfe0110 (GPv2VaultRelayer; CoW Swap)
0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 (USDC) 0xc92e8bdf79f0507f65a392b0ab4667716bfe0110 (GPv2VaultRelayer; CoW Swap)
0xdac17f958d2ee523a2206206994597c13d831ec7 (USDT) 0xc92e8bdf79f0507f65a392b0ab4667716bfe0110 (GPv2VaultRelayer; CoW Swap)
0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 (WETH) 0xc92e8bdf79f0507f65a392b0ab4667716bfe0110 (GPv2VaultRelayer; CoW Swap)
0x2260fac5e5542a773aa44fbcfedf7c193bc2c599 (WBTC) 0xc92e8bdf79f0507f65a392b0ab4667716bfe0110 (GPv2VaultRelayer; CoW Swap)
0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0 (wstETH) 0xc92e8bdf79f0507f65a392b0ab4667716bfe0110 (GPv2VaultRelayer; CoW Swap)

WARNING: the approval of 0x48e5413b73add2434e47504e2a22d14940dbfe78 (INRM) seems unnecessary and is a bit puzzling to me. It is for example not whitelisted for the CowswapOrderSigner.signOrder.sellToken parameter (see ScopeParameterAsOneOf section). Is there something I am missing here, or is this a bug? How did this token get into this list?

ScopeFunction

Besides the (more readable) approvals, this payload makes changes to the Roles module, which is currently enabled on the managed safe.

We can verify these permission changes made by checking the events emitted in the Tenderly simulation, e.g.:

{
    "role": 1
    "targetAddress": "0x889edc2edab5f40e902b864ad4d7ade8e412f9b1"
    "functionSig": "0xd6681042"
    "isParamScoped": [
        0: false
        1: true
    ]
    "paramType": "AAA="
    "paramComp": "AAA="
    "compValue": [
        0: "0x"
        1: "0x0000000000000000000000000efccbb9e2c09ea29551879bd9da32362b32fc89"
    ]
    "options": 0
    "resultingScopeConfig": "3533694129556781213370065774847364703233814209084070120954101330654265344"
}

role here is the index of the Role struct, meaning in this case that there is a target type role change:

struct Role {
    mapping(address => bool) members;
    mapping(address => TargetAddress) targets;
    mapping(bytes32 => uint256) functions;
    mapping(bytes32 => bytes32) compValues;
    mapping(bytes32 => bytes32[]) compValuesOneOf;
}

target and functionSig translate to the following:

>>> target = Contract('0x889edc2edab5f40e902b864ad4d7ade8e412f9b1')
>>> target.name()
'Lido: stETH Withdrawal NFT'
>>> target.selectors['0xd6681042']
'requestWithdrawals'

This corresponds to point (5.) from karpatkey’s specs:

  1. Unstake stETH on Lido;

The function’s full signature is requestWithdrawals(uint256[] _amounts, address _owner), therefore:

  • setting isParamScoped for the parameter with index 0 to false means any amount could be withdrawm, i.e. no limitation
  • isParamScoped for the parameter for index 1 is set to true with value 0x0000000000000000000000000efccbb9e2c09ea29551879bd9da32362b32fc89; the withdrawal can only take place to 0x0EFcCBb9E2C09Ea29551879bd9Da32362b32fc89 which is the managed treasury multisig address

options can give permission to send ether, use delegate_call or both:

enum ExecutionOptions {
    None,
    Send,
    DelegateCall,
    Both
}

We can repeat this for all other scope type events emitted:

contract signature scope
"Lido: stETH Withdrawal NFT" requestWithdrawals(uint256[] _amounts, address _owner) ANY, 0x0efcc
"Lido: stETH Withdrawal NFT" requestWithdrawalsWstETH(uint256[] _amounts, address _owner) ANY, 0x0efcc
"Aave: Pool V3" supply(address asset, uint256 amount, address onBehalfOf) SCOPED*, ANY, 0x0efcc
"Aave: Pool V3" withdraw(address asset, uint256 amount, address onBehalfOf) SCOPED*, ANY, 0x0efcc
"Compound USDC (cUSDCv3)" supply(address asset, uint256 amount) USDC, ANY
"Compound USDC (cUSDCv3)" withdraw(address asset, uint256 amount) USDC, ANY
"CometRewards" claim(address comet, address src, bool shouldAccrue) cUSDCv3, 0x0efcc, ANY
"DsrManager" join(address dst, uint256 wad) 0x0efcc, ANY
"DsrManager" exitAll(address dst) 0x0efcc
"RocketDepositPool" deposit() SEND_ETHER
"Rocket Pool ETH" burn(uint256 _rethAmount) ANY
"RocketSwapRouter" swapTo(uint256 _uniswapPortion, uint256 _balancerPortion, uint256 _minTokensOut, uint256 _idealTokensOut) ANY*, SEND_ETHER
"RocketSwapRouter" swapFrom(uint256 _uniswapPortion, uint256 _balancerPortion, uint256 _minTokensOut, uint256 _idealTokensOut, uint256 _tokensIn) ANY*
"CowswapOrderSigner" signOrder(address sellToken, address buyToken, uint256 sellAmount, uint256 buyAmount, uint32 validTo, uint32 validDuration, uint256 feeAmount, uint256 feeAmountBP, bytes32 kind, bool partiallyFillable, bytes32 sellTokenBalance, bytes32 buyTokenBalance) SCOPED*, SCOPED*, ANY, ANY, ANY, ANY, ANY, ANY, ANY, ANY, ANY, ANY, SEND_ETHER, DELEGATE_CALL

WARNING: deployer of CowswapOrderSigner does not look to be CoW DAO. Who deployed this contract?

WARNING: the feeAmountBP is not scoped, and can therefore be set to something like 99%. There is a potential for malicious/griefing transactions here!

ScopeParameterAsOneOf

Some parameters do not get limited to a single value but to a list of values, as can be read from the ScopeParameterAsOneOf event:

ScopeParameterAsOneOf{
    "role": 1
    "targetAddress": "0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2"
    "functionSig": "0x617ba037"
    "index": "0"
    "paramType": 0
    "compValues": [
        0: "0x0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f"
        1: "0x000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
    ]
    "resultingScopeConfig":"5300541194335184374258569435632574726429567728958937386142040924050423811"
}

So the parameter with index 0 ("Aave: Pool V3".supply.asset) can have two values:

  • 0x6b175474e89094c44da98b954eedeac495271d0f (DAI)
  • 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 (USDC)

"Aave: Pool V3".withdraw.asset can be set to the same two addresses:

  • 0x6b175474e89094c44da98b954eedeac495271d0f (DAI)
  • 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 (USDC)

The CowswapOrderSigner.signOrder.sellToken parameter gets scoped to the following whitelist:

  • 0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9 (AAVE)
  • 0xc00e94cb662c3520282e6f5717214004a7f26888 (COMP)
  • 0x6b175474e89094c44da98b954eedeac495271d0f (DAI)
  • 0xae78736cd615f374d3085123a210448e74fc6393 (rETH)
  • 0x20bc832ca081b91433ff6c17f85701b6e92486c5 (rETH2)
  • 0xfe2e637202056d30016725477c5da089ab0a043a (sETH2)
  • 0xae7ab96520de3a18e5e111b5eaab095312d7fe84 (stETH)
  • 0x48c3399719b582dd63eb5aadf12a40b4c3f52fa2 (SWISE)
  • 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 (USDC)
  • 0xdac17f958d2ee523a2206206994597c13d831ec7 (USDT)
  • 0x2260fac5e5542a773aa44fbcfedf7c193bc2c599 (WBTC)
  • 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 (WETH)
  • 0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0 (wstETH)

And for CowswapOrderSigner.signOrder.buyToken:

  • 0x6b175474e89094c44da98b954eedeac495271d0f (DAI)
  • 0xae78736cd615f374d3085123a210448e74fc6393 (rETH)
  • 0xfe2e637202056d30016725477c5da089ab0a043a (sETH2)
  • 0xae7ab96520de3a18e5e111b5eaab095312d7fe84 (stETH)
  • 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 (USDC)
  • 0xdac17f958d2ee523a2206206994597c13d831ec7 (USDT)
  • 0x2260fac5e5542a773aa44fbcfedf7c193bc2c599 (WBTC)
  • 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 (WETH)
  • 0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0 (wstETH)

Conclusion

The INRM token approval is puzzling but not necessarily a security issue.

For the CoW swap order signing however, I would recommend either (1) a scope on the feeAmountBP parameter or (2) a new (official) deployment of the signer contract which has a tighter sanity check on the fee (ref) to prevent getting assets drained through unrealistic fees.

4 Likes