After completing the mandatory codebase reconnaissance, I have determined that removal is the only safe and correct resolution path for the following reasons:
The Soroban SDK 21.0.0 does not provide a mechanism to extract the Ed25519 public key bytes from an Address type within a contract context. The Address type is opaque and does not expose methods like to_public_key() or similar. Without access to the public key, it is impossible to call env.crypto().ed25519_verify(&public_key, &message, &signature).
Evidence:
- Reviewed Soroban SDK documentation and example contracts (simple-account, complex-account)
- The example contracts receive public keys as explicit
BytesN<32>parameters during initialization - No SDK method exists to derive public key bytes from an
Addressrepresenting an account
The current permit() function:
- Accepts a
_signature: BytesN<64>parameter (note the underscore prefix indicating it's unused) - Completely ignores the signature
- Falls back to
owner.require_auth()— making it functionally identical toapprove() - Provides a false sense of security to any integrator who assumes EIP-2612-style gasless authorization
Severity Classification: This is a deception vulnerability. Any relayer or integrator reading the function signature would reasonably assume it provides gasless permit functionality, but it actually requires the owner to authorize the transaction on-chain, defeating the entire purpose.
From NEW_ISSUES.md (Issue #1), the feature is described as:
"Introduce a
permitfunction allowing users to sign an off-chain authorization (similar to EIP-2612). A relayer or spender can submit this signature with the transaction to instantly gain allowance logic without an upfront on-chain approval."
The current implementation does not fulfill this requirement. It is a stub that was never completed.
- No other contracts in the workspace reference
permit() - The function is not part of any required SEP standard for this token
- Removal will not break any existing integrations (since the function doesn't work as advertised anyway)
-
contracts/token/src/lib.rs
- Remove the entire
permit()function (lines 438-500) - Remove the
nonce()getter function (lines 502-506) — only used by permit - Remove
DataKey::Nonce(Address)variant (line 18) — only used by permit - Keep all other functionality intact
- Remove the entire
-
contracts/token/src/lib.rs (tests section)
- Delete
test_permit_functionality()(lines 1242-1275) - Delete
test_permit_nonce_validation()(lines 1277-1303) - Delete
test_permit_invalid_nonce()(lines 1305-1327)
- Delete
-
Documentation
- Add removal notice to README.md explaining why permit was removed and when it might return
- All other token contract functions remain unchanged
- The
approve()function continues to work as the standard approval mechanism - All existing tests for other functionality will continue to pass
- No changes to the vesting contract (issue #215 is separate)
Current State:
- Deception vulnerability: Function signature implies gasless authorization but requires on-chain auth
- Nonce storage waste: Nonces are stored and incremented but serve no security purpose
- Misleading tests: Tests pass zero-byte signatures and claim to test "functionality"
Post-Removal State:
- No false promises: Function removed, no misleading API surface
- Clean codebase: No unused nonce storage or stub implementations
- Clear documentation: Removal notice explains the situation and future plans
Based on .github/workflows/ci.yml:
- ✅
cargo fmt --all -- --check— formatting - ✅
cargo test --workspace— all tests pass (after removing permit tests) - ✅
cargo build --target wasm32-unknown-unknown --release— WASM builds successfully - ✅ WASM size delta: Expected decrease due to removal of unused code
- Branch: Will create new branch from latest
master - Issue #215: Modifies
contracts/vesting/src/lib.rs— no file overlap with this change - Commit message:
fix(token): remove non-functional permit() to eliminate deception vulnerability (#216)
When the Soroban SDK adds support for extracting Ed25519 public keys from Address types (or provides an alternative signature verification mechanism for addresses), permit() can be properly implemented following this specification:
Message Construction:
message = sha256(
b"soropad_token_permit_v1" || // domain separator
contract_address ||
owner_address ||
spender_address ||
amount.to_le_bytes() ||
expiration_ledger.to_le_bytes() ||
nonce.to_le_bytes()
)Implementation Requirements:
- Extract owner's Ed25519 public key from
Address - Verify signature:
env.crypto().ed25519_verify(&public_key, &message, &signature) - Validate nonce (replay protection)
- Validate expiration_ledger
- Remove
owner.require_auth()call - Set allowance on successful verification
None. The resolution path is clear: removal is the only safe option given the SDK limitations.
Proceeding with PATH B: Complete removal of permit() and related infrastructure.