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
- 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.
- 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.
- 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.