Bug Report – CDaiDelegate (Compound CDai Delegate)
Severity: HIGH (Critical if admin key is compromised)
1. Summary
The CDaiDelegate contract allows the admin to invoke _becomeImplementation multiple times, each time resetting the internal references to the DAI adapter (daiJoinAddress) and the DSR pot (potAddress). This function performs critical state changes, grants unlimited allowances, and triggers a full cash‑in of the contract’s DAI balance via doTransferIn. If an attacker gains access to the admin key, they can re‑initialize the contract with malicious daiJoin/pot contracts, causing unauthorized transfer of all underlying assets out of the vault.
2. Detailed Findings
| # |
Issue |
Location |
Description |
Impact |
| 1 |
Unrestricted Re‑initialisation by Admin |
_becomeImplementation(bytes) (external) → _becomeImplementation(address,address) (internal) |
The external function only checks msg.sender == admin. It does not enforce a one‑time initialization or a “finalised” flag. An admin (or anyone who compromises the admin key) can call this function repeatedly, swapping the contract’s daiJoinAddress, potAddress, and vatAddress to attacker‑controlled contracts. This re‑initialisation also: • Calls dai.approve(daiJoinAddress, type(uint).max) granting unlimited allowance to the new daiJoin. • Calls vat.hope(potAddress) and vat.hope(daiJoinAddress), giving the new contracts permission to move funds inside the Vat. • Executes pot.drip() followed by doTransferIn(address(this), 0), which pulls all DAI from the contract into the new (potentially malicious) daiJoin. |
Critical – Full loss of underlying DAI if admin is compromised or a malicious admin is set. |
| 2 |
Potential Dust / Rounding Issue on Resign |
_resignImplementation() |
After exiting the pot, the contract computes bal / RAY (where RAY = 10**27) and calls daiJoin.exit(address(this), bal / RAY). Any remainder (bal % RAY) stays locked inside the Vat, creating permanent dust. While not a direct security breach, it leads to loss of funds over time. |
Low‑Medium – Economic loss, not a exploitable bug. |
| 3 |
Missing Checks for External Contract Types |
_becomeImplementation(address,address) |
No validation that daiJoinAddress_ and potAddress_ point to contracts implementing the expected interfaces (DaiJoinLike, PotLike). An admin could unintentionally (or maliciously) set them to non‑contract addresses, causing subsequent external calls to revert and potentially lock the contract. |
Medium – Operational risk / denial‑of‑service. |
| 4 |
Unlimited ERC‑20 Allowance |
_becomeImplementation(address,address) – dai.approve(daiJoinAddress, type(uint).max) |
Grants the daiJoin contract unlimited spending rights over the contract’s DAI balance. If the daiJoin contract is compromised, an attacker can drain all DAI without needing to trigger a re‑initialisation. |
Medium – Amplifies impact of issue #1. |
3. exploitation Scenario (High Severity)
- Attacker obtains admin private key (phishing, key‑reuse, or insider threat).
- Calls
_becomeImplementation with attacker‑controlled daiJoin and pot addresses.
- Unlimited allowance is granted to the malicious
daiJoin.
vat.hope gives the malicious pot permission to move funds inside the Vat.
pot.drip() (no‑op for attacker contract) followed by doTransferIn(address(this), 0) triggers the internal logic that transfers the entire DAI balance from the contract into the attacker‑controlled daiJoin.
- Attacker then withdraws the DAI from the
daiJoin to any address they control, effectively stealing all underlying assets.
Because the contract lacks a “initialized‑once” guard, the above can be repeated at any time, even after the contract has been in operation for months.
4. Recommendations
-
One‑Time Initialization Guard
bool private _initialized;
function _becomeImplementation(bytes memory data) external override {
require(!_initialized, "Already initialized");
require(msg.sender == admin, "only admin");
// … existing logic …
_initialized = true;
}
This prevents any further re‑initialisation after the first successful call.
-
Restrict Re‑initialisation to a Timelocked Upgrade Process
If future upgrades are required, adopt a timelock (e.g., Compound’s Timelock) or a governance‑controlled upgrade path rather than direct admin calls.
-
Validate External Addresses
Add checks such as require(daiJoinAddress_.isContract(), "daiJoin must be a contract"); and analogously for potAddress_. Use Address library from OpenZeppelin.
-
Limit ERC‑20 Allowance
Instead of type(uint).max, approve only the exact amount needed for the subsequent doTransferIn call, or revoke allowances after use.
-
Handle Rounding Dust on Resign
Emit an event warning about remaining dust and, if possible, provide a function to sweep the residual vat.dai balance.
-
Add Re‑entrancy Guard (defensive)
Though the current external calls are not expected to invoke re‑entrancy, adding nonReentrant from OpenZeppelin’s ReentrancyGuard on _becomeImplementation and _resignImplementation offers additional safety.
5. Conclusion
The contract functions correctly under the assumption that the admin key remains secure. However, the ability for the admin to arbitrarily re‑initialize the delegate without any guard constitutes a high‑severity vulnerability. If the admin key is compromised, an attacker can redirect all DAI assets to malicious contracts and exfiltrate them. Implementing a one‑time initialization guard, address validation, and tighter allowance management mitigates this risk.
Overall Verdict: HIGH (critical if admin is compromised)
Prepared by:
[Your Name] – Senior Web3 Security Auditor
Date: 2026‑03‑21
RECOMMENDATION: Immediate patch required. Bug Bounty Payout Address (ERC20): 0xe744f6791a685b0A0cC316ED44375B69361c837F
Bug Report – CDaiDelegate (Compound CDai Delegate)
Severity: HIGH (Critical if admin key is compromised)
1. Summary
The
CDaiDelegatecontract allows the admin to invoke_becomeImplementationmultiple times, each time resetting the internal references to the DAI adapter (daiJoinAddress) and the DSR pot (potAddress). This function performs critical state changes, grants unlimited allowances, and triggers a full cash‑in of the contract’s DAI balance viadoTransferIn. If an attacker gains access to the admin key, they can re‑initialize the contract with maliciousdaiJoin/potcontracts, causing unauthorized transfer of all underlying assets out of the vault.2. Detailed Findings
_becomeImplementation(bytes)(external) →_becomeImplementation(address,address)(internal)msg.sender == admin. It does not enforce a one‑time initialization or a “finalised” flag. An admin (or anyone who compromises the admin key) can call this function repeatedly, swapping the contract’sdaiJoinAddress,potAddress, andvatAddressto attacker‑controlled contracts. This re‑initialisation also:• Calls
dai.approve(daiJoinAddress, type(uint).max)granting unlimited allowance to the newdaiJoin.• Calls
vat.hope(potAddress)andvat.hope(daiJoinAddress), giving the new contracts permission to move funds inside the Vat.• Executes
pot.drip()followed bydoTransferIn(address(this), 0), which pulls all DAI from the contract into the new (potentially malicious)daiJoin._resignImplementation()bal / RAY(whereRAY = 10**27) and callsdaiJoin.exit(address(this), bal / RAY). Any remainder (bal % RAY) stays locked inside the Vat, creating permanent dust. While not a direct security breach, it leads to loss of funds over time._becomeImplementation(address,address)daiJoinAddress_andpotAddress_point to contracts implementing the expected interfaces (DaiJoinLike,PotLike). An admin could unintentionally (or maliciously) set them to non‑contract addresses, causing subsequent external calls to revert and potentially lock the contract._becomeImplementation(address,address)–dai.approve(daiJoinAddress, type(uint).max)daiJoincontract unlimited spending rights over the contract’s DAI balance. If thedaiJoincontract is compromised, an attacker can drain all DAI without needing to trigger a re‑initialisation.3. exploitation Scenario (High Severity)
_becomeImplementationwith attacker‑controlleddaiJoinandpotaddresses.daiJoin.vat.hopegives the maliciouspotpermission to move funds inside the Vat.pot.drip()(no‑op for attacker contract) followed bydoTransferIn(address(this), 0)triggers the internal logic that transfers the entire DAI balance from the contract into the attacker‑controlleddaiJoin.daiJointo any address they control, effectively stealing all underlying assets.Because the contract lacks a “initialized‑once” guard, the above can be repeated at any time, even after the contract has been in operation for months.
4. Recommendations
One‑Time Initialization Guard
This prevents any further re‑initialisation after the first successful call.
Restrict Re‑initialisation to a Timelocked Upgrade Process
If future upgrades are required, adopt a timelock (e.g., Compound’s
Timelock) or a governance‑controlled upgrade path rather than direct admin calls.Validate External Addresses
Add checks such as
require(daiJoinAddress_.isContract(), "daiJoin must be a contract");and analogously forpotAddress_. UseAddresslibrary from OpenZeppelin.Limit ERC‑20 Allowance
Instead of
type(uint).max, approve only the exact amount needed for the subsequentdoTransferIncall, or revoke allowances after use.Handle Rounding Dust on Resign
Emit an event warning about remaining dust and, if possible, provide a function to sweep the residual
vat.daibalance.Add Re‑entrancy Guard (defensive)
Though the current external calls are not expected to invoke re‑entrancy, adding
nonReentrantfrom OpenZeppelin’sReentrancyGuardon_becomeImplementationand_resignImplementationoffers additional safety.5. Conclusion
The contract functions correctly under the assumption that the admin key remains secure. However, the ability for the admin to arbitrarily re‑initialize the delegate without any guard constitutes a high‑severity vulnerability. If the admin key is compromised, an attacker can redirect all DAI assets to malicious contracts and exfiltrate them. Implementing a one‑time initialization guard, address validation, and tighter allowance management mitigates this risk.
Overall Verdict: HIGH (critical if admin is compromised)
Prepared by:
[Your Name] – Senior Web3 Security Auditor
Date: 2026‑03‑21
RECOMMENDATION: Immediate patch required. Bug Bounty Payout Address (ERC20): 0xe744f6791a685b0A0cC316ED44375B69361c837F