Organization
- @coinbase
Engagement Type
Cantina Reviews
Period
-
Repositories
Findings
Informational
3 findings
2 fixed
1 acknowledged
Informational3 findings
SpendRouter missing spendWithWithdraw() routing support
Description
SpendPermissionManageralready supportsspendWithWithdraw(), which allows a spender to atomically top up an account fromMagicSpendand then spend.function spendWithWithdraw( SpendPermission memory spendPermission, uint160 value, MagicSpend.WithdrawRequest memory withdrawRequest ) external requireSender(spendPermission.spender) { // check spend token and withdraw asset are the same if ( !(spendPermission.token == NATIVE_TOKEN && withdrawRequest.asset == address(0)) && spendPermission.token != withdrawRequest.asset ) { revert SpendTokenWithdrawAssetMismatch(spendPermission.token, withdrawRequest.asset); } // check spend value is not less than withdraw request amount if (withdrawRequest.amount > value) { revert SpendValueWithdrawAmountMismatch(value, withdrawRequest.amount); } // check withdraw request nonce postfix matches spend permission hash postfix. bytes32 permissionHash = getHash(spendPermission); if (uint128(withdrawRequest.nonce) != uint128(uint256(permissionHash))) { revert InvalidWithdrawRequestNonce(uint128(withdrawRequest.nonce), uint128(uint256(permissionHash))); } _useSpendPermission(spendPermission, value); _execute({ account: spendPermission.account, target: MAGIC_SPEND, value: 0, data: abi.encodeWithSelector(MagicSpend.withdraw.selector, withdrawRequest) }); _transferFrom(spendPermission.token, spendPermission.account, spendPermission.spender, value); }However,
SpendRouteronly wrapsspend/approveWithSignature+spend, so flows that rely onMagicSpendcannot preserve the router’sexecutor/recipientbinding in a single transaction. As a result, integrators must either build a custom wrapper or fall back to a two-step flow, losing atomicity and weakening the intended periphery UX.Recommendation
Add router-level wrappers such as
spendWithWithdrawAndRoute()and, if needed,spendWithWithdrawAndRouteWithSignature(), while keeping the existingexecutorandrecipientchecks unchanged.Coinbase
Accepting suggestion. Fix is implemented in https://github.com/coinbase/spend-permissions/pull/81
Cantina
Fixed as recommended.
Missing zero-address check for executor allows permissions that can never be executed
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
0xRajeev
Description
decodeExtraData()validates thatextraDatais exactly 64 bytes but does not check that the decoded executor address is non-zero. InspendAndRoute()andspendAndRouteWithSignature(), the only post-decode validation is on recipient and executor is used directly in themsg.sendercomparison without a zero-address guard. Sincemsg.sendercan never beaddress(0)in the EVM, a permission accidentally encoded with a zero executor can never be executed.While
encodeExtraData()already guards against this for users who go through the helper, users who manually craftextraDataare susceptible to this scenario.Recommendation
Consider adding an explicit zero-address check on executor after decoding, either inside
decodeExtraData()or at the call sites alongside the existing recipient check.Coinbase
Acknowledged, won't fix. Since a permission thus constructed poses no security risk, we'll avoid placing the extra check on the happy path.
Cantina
Acknowledged.
Missing zero-address check for spendPermissionManager in constructor
Description
The constructor accepts a
SpendPermissionManagerargument and assigns it directly to thePERMISSION_MANAGERimmutable without validating that the address is non-zero. SincePERMISSION_MANAGERis immutable, a deployment with a zero address permanently bricks the contract, with no recovery path short of redeployment. Given thatSpendRouteris intended as a singleton, a misconfigured deployment would require redeployment and redistribution of the new address to all dependent systems.While the deployment script currently deploys
SpendPermissionManagerimmediately beforeSpendRouterand passes the live address directly, it makes an accidental EOA or undeployed address unlikely in practice. However, a zero-address or a zero-code guard is a defensive check for any misconfiguration risk.Recommendation
Consider adding an explicit zero-address or zero-code guard in the constructor before assigning the immutable.