Intermediate

Approval Patterns & Token Permissions — Kudos build — permit UX

Understand token flows.

Developer Intermediate
5/6 — Approval Patterns & Token Permissions — Kudos build — permit UX

Objective Remove extra approve clicks by adding EIP-2612 permit to Kudos and wiring it into the app’s stake flow.

Flow Extend the token with ERC20Permit, call permit then transfer in one path, and surface revoke/limit guidance in the UI.

What to write

  1. Add ERC20Permit
    Extend Kudos with permit support.
    contract KudosToken is ERC20Permit, AccessControl {
        constructor() ERC20("Kudos", "KUDOS") ERC20Permit("Kudos") {
            _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        }
    }

    Adds typed-data signatures without altering the ERC-20 surface.

  2. Permit + stake path
    Consume signature then transfer.
    function stakeWithPermit(PermitData calldata p, uint256 amount) external {
        kudos.permit(msg.sender, address(this), p.value, p.deadline, p.v, p.r, p.s);
        kudos.transferFrom(msg.sender, address(this), amount);
        stakes[msg.sender] += amount;
        emit Staked(msg.sender, amount);
    }

    Collapses approval + stake into one flow with signature validation.

  3. Surface revokes
    List allowances and link to revoke/limit in the UI.

    Keeps users aware of spenders; nudges toward least privilege.

Why Permits collapse approval + stake into one flow, reducing allowance risk and friction while keeping signature validation explicit.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC20, ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";

contract KudosToken is ERC20Permit, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    constructor() ERC20("Kudos", "KUDOS") ERC20Permit("Kudos") {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(MINTER_ROLE, msg.sender);
    }

    function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }
}

contract KudosStaking {
    struct PermitData { uint256 value; uint256 deadline; uint8 v; bytes32 r; bytes32 s; }
    KudosToken public immutable kudos;
    mapping(address => uint256) public stakes;

    event Staked(address indexed user, uint256 amount);

    constructor(address token) { kudos = KudosToken(token); }

    function stakeWithPermit(PermitData calldata p, uint256 amount) external {
        kudos.permit(msg.sender, address(this), p.value, p.deadline, p.v, p.r, p.s);
        kudos.transferFrom(msg.sender, address(this), amount);
        stakes[msg.sender] += amount;
        emit Staked(msg.sender, amount);
    }
}

Next Put the whole stack behind an upgradeable proxy.

← Previous section
Next section →