[BIP-442] Permissions Preset Update Request #2

PR with Payload

Abstract

This proposal introduces additional actions and strategies to the portion of the treasury managed by karpatkey (as per BIP-103), aiming to enhance the treasury’s performance, adaptability, and diversification.

Motivation

Permissions granted to karpatkey to manage the BalancerDAO treasury are not static, and require regular updates to effectively respond to evolving market conditions and to capitalise on emerging opportunities.

As it’s customary, any changes to the preset permissions must be executed by the DAO.

Specification

Permissions that will be added in this proposal are:

  1. Deposit USDC on Compound v3;
  2. Deposit DAI on AAVE v3;
  3. Deposit USDC on AAVE v3;
  4. Deposit DAI on MakerDAO’s Dai Savings Rate (DSR) module;
  5. Unstake stETH on Lido;
  6. Stake (and unstake) ETH on Rocket Pool (rETH);
  7. Swap on Cow Swap

Payload to be executed from the Ethereum Avatar Safe

Audit considerations

We propose an updated version of the Preset permissions - Balancer Treasury document that shows all the permissions granted to karpatkey, including the ones that are being requested now, highlighted in green.

Reviewing and providing feedback on the preset update payload would be a great service to the Balancer community. We welcome anyone who is willing to take the time to verify the content and encourage community members to share their feedback with us.

karpatkey’s tech team is diligently working on enhancing the auditing process. Our primary goal is to provide a clearer visual representation of potential Zodiac preset modifications before implementing any proposed payload. We are committed to keeping the community informed about our progress and developments.

Additional references

Avatar Safe address: 0x0EFcCBb9E2C09Ea29551879bd9Da32362b32fc89

Roles Modifier address: 0xd8dd9164E765bEF903E429c9462E51F0Ea8514F9

Manager Safe address: 0x60716991aCDA9E990bFB3b1224f1f0fB81538267

5 Likes

Proposed Actions Overview

1. Deposit USDC on Compound v3

Compound v3 is a streamlined protocol version emphasising security, capital efficiency, and user experience. Following a proposal executed on September 18th, v2 is being phased out, with measures in place to transition users to v3. OpenZeppelin and ChainSecurity have audited v3. As of the day of this writing, $319M USDC is deposited in v3, offering an APR of 4.3%, inclusive of COMP token rewards.

2. Deposit DAI on AAVE v3

This strategy aims to diversify stablecoin holdings via AAVE v3, a leading decentralised money market with a strong link to the Balancer DAO. AAVE v3 was officially deployed on mainnet in January 2023 after being live across six networks since March 2022. This version introduces features for enhanced capital efficiency and decentralisation, coupled with new risk management tools. Comprehensive audits by firms like Trail of Bits, ABDK, Peckshield, OpenZeppelin, SigmaPrime, and formal verification by Certora. Currently, AAVE v3 holds $51.6M DAI with an expected APY of 3.82%.

3. Deposit USDC on AAVE v3

Same logic as above but for USDC. As of the day of this writing, the total USDC supplied on AAVE v3 is $299M, and the expected APY is 2.86%.

4. Deposit DAI on MakerDAO’s DAI Savings Rate (DSR) module

MakerDAO, renowned for creating DAI, is a leading DAO in the DeFi sector. A recent DSR rate hike in May and the introduction of eDSR (enhanced DSR) in August make it an attractive strategy from a risk/reward perspective. Currently, eDSR offers an APR of 5%. By diversifying the Balancer treasury’s stablecoin allocation and capitalising on the higher yield offered by one of the most battle-tested protocols, the investment approach can be optimised.

5. Unstake stETH on Lido

Post the Shapella fork in April, users can now unstake or withdraw staked ETH from the different LST providers. This action was not part of the previous preset update request to the Balancer DAO due to its timing.

6. Stake (and unstake) ETH on Rocket Pool (rETH)

To diversify LST holdings, we suggest adding RocketPool to the whitelist for ETH staking and unstaking. RocketPool ranks third in the liquid staking market, holding about 691,000 ETH, or 6% of the total market. In market share, Rocket Pool’s rETH trails Lido’s stETH (76%) and Coinbase’s cbETH (11%). Despite slightly lower liquidity for rETH to ETH swaps on major DEXes, Balancer DAO treasury’s allocated amount should face minimal slippage. For example, DeFi Llama indicates a swap of ~5,700 rETH for ETH would have under 0.1% slippage. Currently, rETH offers an APR of 3.26%.

7. Swap on Cow Swap

Thanks to the efforts of the Gnosis Builders and the karpatkey tech team, Cow Swap can now be seamlessly integrated with the Zodiac Roles Modifier. This enhancement allows for the exclusive swapping of only those tokens that are included in the whitelisted strategies.

5 Likes

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

Thank you very much for completing this review.

1 Like

Hi @gosuto,

We deeply appreciate your time and effort in auditing the payload and offering invaluable insights to the community.

Update on Cow Protocol

Firstly, we’d like to bring to everyone’s attention a recent update to the Cow protocol. This update renders the current version of the CowswapOrderSigner contract incompatible with the Zodiac Roles Modifier setup we employ for non-custodial management of Balancer’s treasury. As a result, we’re revising the payload to exclude all Cow-related elements.

Addressing Your Points

While the removal of Cow from the payload renders some of your concerns moot, we believe it’s important to address the excellent points you’ve raised:

  1. INRM Token Approval

The inclusion of the INRM token address (0x48e5413b73add2434e47504e2a22d14940dbfe78) was inadvertent. The intended token was SWISE (0x48C3399719B582dD63eB5AADf12A40B4C3f52FA2). It’s crucial to note that this was merely an approval action and, as it wasn’t whitelisted for the CowswapOrderSigner.signOrder.sellToken parameter, swaps involving this token would have been impossible. The whitelisting of the SWISE token in the said parameter clearly indicates our original intent.

  1. CowSwapOrderSigner Deployment

The CowswapOrderSigner contract was a collaborative effort between Gnosis Guild (the creators of Zodiac tools) and Cow developers. The contract, which is part of the Gnosis repository, has undergone rigorous auditing by Gnosis and was deployed by an address owned by karpatkey.

  1. Non-Scoping of feeAmountBP

Your observation regarding the non-scoping of feeAmountBP was astute. Post internal discussions between our Strategy & Ops and Tech teams, we concur that limiting this parameter would enhance the robustness of the preset. Once a future version of the CowswapOrderSigner contract is available and Cow can be reintegrated into the Zodiac environment, we will certainly consider scoping this parameter.

3 Likes

i think The inclusion of the INRM token address (0x48e5413b73add2434e47504e2a22d14940dbfe78 ) was inadvertent. The intended token was SWISE (0x48C3399719B582dD63eB5AADf12A40B4C3f52FA2 ). It’s crucial to note that this was merely an approval action and, as it wasn’t whitelisted for the CowswapOrderSigner.signOrder.sellToken parameter, swaps involving this token would have been impossible. The whitelisting of the SWISE token in the said parameter clearly indicates our original intent.