Missing modifiers, wrong msg.sender checks, default-public functions, missing onlyOwner / onlyRole / onlyDAO authorization.
64
77%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./packages/decepticon/decepticon/skills/standard/contracts/access-control/SKILL.mdAccess control bugs are the most boring class — and the most common in production audits. They're cheap to find with grep + LSP. They drain millions when missed (LeetSwap, Audius, Saddle, Akropolis).
# Functions that are NOT view/pure and NOT internal/private
grep -rE 'function [a-zA-Z_]+\(.*\)(public|external)' src/ | grep -v 'view\|pure'
# Or via slither
slither . --print function-summaryonlyOwner, onlyRole, onlyDAO, custom auth)?Common modifier patterns + bugs:
| Pattern | Bug |
|---|---|
require(msg.sender == owner) | owner settable by anyone? setOwner unprotected? |
require(msg.sender == tx.origin) | Wrong — tx.origin breaks meta-transactions, AND it's phishable |
_msgSender() in OZ ERC2771Context | Forwarder trusted but anyone can forward — does the contract validate the forwarder? |
onlyRole(MINTER) | Who can grant MINTER? Is the admin a multisig or single key? |
require(initialized == false) | Initializer can be called twice if initialized writeable elsewhere |
require(block.timestamp > deployTime + 24 hours) | Time-based is often a fake delay — check if deployTime is settable |
require(approvedSigners[msg.sender]) | Approval list managed by single key? |
# Functions accidentally external (default in Solidity <0.5)
grep -rn 'function [a-zA-Z_]*[^ ]* *{' src/ | grep -v 'internal\|private\|public\|external'
# msg.sender == tx.origin (phishable)
grep -rn 'tx.origin' src/
# Reentrancy in access-control checks
grep -rn 'onlyOwner.*nonReentrant' src/ # both? often wrong order
# `delegatecall` without auth gate
grep -rn 'delegatecall' src/
# `selfdestruct` available
grep -rn 'selfdestruct\|suicide(' src/// VULNERABLE
function initialize(address _owner) external {
owner = _owner;
// missing: require(!initialized); initialized = true;
}
// VULNERABLE (proxy implementation)
contract Impl {
constructor() { ... } // NEVER RUNS in proxy context
// initialize() should set proxy state but doesn't gate
}
// SAFE
function initialize(address _owner) external initializer {
// OZ's initializer modifier enforces single-call
__Ownable_init(_owner);
}For UUPS/Transparent proxies: check _authorizeUpgrade is overridden
and gated. Default OZ override is empty (revert).
Transparent proxy admin function selectors collide w/ impl function selectors → admin functions become uncallable, OR impl functions are shadowed by admin. Check w/ slither:
slither-check-erc src/Impl.sol --erc ERC1967For AccessControl:
DEFAULT_ADMIN_ROLE is the admin of all roles by defaultCheck role hierarchy:
grep -rn '_setRoleAdmin\|_setupRole\|grantRole' src/function test_unauth_call() public {
address attacker = address(0xBEEF);
vm.prank(attacker);
// Call the function that should require auth
target.dangerousFunction(arg1, arg2);
// Assert state change happened
assertEq(target.criticalParam(), expectedManipulatedValue);
// No revert = vulnerable
}
function test_role_takeover() public {
address attacker = address(0xBEEF);
vm.startPrank(attacker);
// If grantRole is callable by anyone
target.grantRole(target.MINTER_ROLE(), attacker);
assertTrue(target.hasRole(target.MINTER_ROLE(), attacker));
// Now exploit MINTER_ROLE
target.mint(attacker, 1_000_000 ether);
}
function test_init_re_entry() public {
// Call initialize twice
target.initialize(address(this));
vm.expectRevert("Initializable: contract is already initialized");
target.initialize(attacker);
}
function test_upgrade_no_auth() public {
address attacker = address(0xBEEF);
address malicious = address(new MaliciousImpl());
vm.prank(attacker);
target.upgradeTo(malicious);
// Now any call goes to malicious — drain
assertEq(target.totalSupply(), 0);
}| Bug | Severity |
|---|---|
Unauth withdraw / mint / transferOwnership | Critical |
Unauth upgradeTo (proxy) | Critical (escalates to any) |
Unauth setOracle / setFee | High (DoS or value manipulation) |
tx.origin auth + phishable | High |
| Initializer re-entry | High (proxy takeover) |
| Role hierarchy lets non-admin grant admin | High |
| Function defaults to external (Solidity <0.5 only) | High |
| Selector collision in proxy | High (function unreachable) |
// Use OpenZeppelin patterns
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
contract MyContract is AccessControlUpgradeable, UUPSUpgradeable {
bytes32 public constant MINTER = keccak256("MINTER");
function initialize(address admin) external initializer {
__AccessControl_init();
_grantRole(DEFAULT_ADMIN_ROLE, admin);
}
function mint(address to, uint256 amt) external onlyRole(MINTER) {
_mint(to, amt);
}
function _authorizeUpgrade(address) internal override onlyRole(DEFAULT_ADMIN_ROLE) {}
}e34afba
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.