[PAXG] ERC20 Token Smart Contract Domain Community Assessment

General Information

Risk Summary

  • Does the contract implement the ERC20 token standards?
    Yes, the contract implements all the required ERC20 functions.
    PAXG is a pausable erc20 token with burn and mint controlled by a central SupplyController.
    The contract also has additional asset protection functions.
  • Risk analysis: Medium

Technical Information

  • Compiler version: v0.4.24+commit.e67f0147
  • Decimals: 18
  • Overflow checks: Yes, the contract uses a SafeMath library.
  • Mitigation against allowance race-condition:
    No, the approve function does have a race condition
  • Upgradeable contract patterns: yes, contract provides UpgradeabilityProxy
  • Access control or restriction lists: ability to freeze/pause wallets, seize tokens from wallets
  • Non-standard features or behaviors: upgradeability, admin proxy, pausing and freezing,
    minting and burning, asset protection functions, BetaDelegateTransfer allowing end user to pay gas for token transfers.

Formal Verification Considerations:

  • Does transfer have simple semantics? Yes, seems standard
  • Does transferFrom have simple semantics? Yes, seems standard
  • Can balances be arbitrarily modified by some actor?
    AssetProtectionRole can freeze/unfreeze wallets and seize tokens
  • Are there any external calls?
    Only the Proxy which can change the contract implementation.
    There is also an external function “onlyOwner” which uses the AssetProtectionRole
    to seize PAXG

Testnet Information

No Testnet Information available

Contract Logic Summary

The token uses a Proxy contract which links to an implementation contract. The implementation
contract is an upgradeable, freezable, contract with special asset protection functions. At its core the
implementation is a standard erc20 contract which uses the SafeMath contract. But these additional
functions are added to comply with regulation, and due to being a physical asset custodial token.

The contract is upgraded by pointing the Proxy contract to a new implementation contract. This is done
by actors with administrative permissions.

Administrative Addresses

Below is a list of several addresses related to token management:

feeRecipient: 0x611479ce1d3d457ffe9fb2abbaf8d3fe2ad8fffb
feeController: 0x5195427ca88df768c298721da791b93ad11eca65
owner: 0xb87cec7baa2ce4a055f8563e9cc5a210cedc329f
supplyController: 0x5195427ca88df768c298721da791b93ad11eca65
assetProtectionRole: 0xb87cec7baa2ce4a055f8563e9cc5a210cedc329f
All of these addresses are EOAs with private keys, and no associated code. This presents more risk because there is no way to know who holds these keys and how the security is being handled.

Contract Risk Summary

This is a medium risk contract. The ERC20 functions are implemented to the industry standard and uses upgradeable design patterns. The ERC20 allowance race condition is noted in their code and not mitigated. The contract makes use of a SafeMath library to prevent overflows & underflows. The contract also has pausable, freezable features, and the ability for an administrator to seize funds
from wallets. These features are similar to other real asset and custodial tokens.

Supporting Materials

BetaDelegateTransfer function: https://github.com/paxosglobal/paxos-gold-contract#betadelegatetransfer

Contracts Description Table

Proxy Contract:

Contract Type Bases
Function Name Visibility Mutability Modifiers
Proxy Implementation
External :exclamation: :dollar: NO❗️
_implementation Internal :lock:
_delegate Internal :lock: :stop_sign:
_willFallback Internal :lock: :stop_sign:
_fallback Internal :lock: :stop_sign:
AddressUtils Library
isContract Internal :lock:
UpgradeabilityProxy Implementation Proxy
Public :exclamation: :stop_sign: NO❗️
_implementation Internal :lock:
_upgradeTo Internal :lock: :stop_sign:
_setImplementation Private :closed_lock_with_key: :stop_sign:
AdminUpgradeabilityProxy Implementation UpgradeabilityProxy
Public :exclamation: :stop_sign: UpgradeabilityProxy
admin External :exclamation: ifAdmin
implementation External :exclamation: ifAdmin
changeAdmin External :exclamation: :stop_sign: ifAdmin
upgradeTo External :exclamation: :stop_sign: ifAdmin
upgradeToAndCall External :exclamation: :dollar: ifAdmin
_admin Internal :lock:
_setAdmin Internal :lock: :stop_sign:
_willFallback Internal :lock: :stop_sign:

Implementation Contract:

Contract Type Bases
Function Name Visibility Mutability Modifiers
SafeMath Library
sub Internal :lock:
add Internal :lock:
mul Internal :lock:
div Internal :lock:
PAXGImplementation Implementation
initialize Public :exclamation: :stop_sign: NO❗️
Public :exclamation: :stop_sign: NO❗️
initializeDomainSeparator Public :exclamation: :stop_sign: NO❗️
totalSupply Public :exclamation: NO❗️
transfer Public :exclamation: :stop_sign: whenNotPaused
balanceOf Public :exclamation: NO❗️
transferFrom Public :exclamation: :stop_sign: whenNotPaused
approve Public :exclamation: :stop_sign: whenNotPaused
allowance Public :exclamation: NO❗️
_transfer Internal :lock: :stop_sign:
proposeOwner Public :exclamation: :stop_sign: onlyOwner
disregardProposeOwner Public :exclamation: :stop_sign: NO❗️
claimOwnership Public :exclamation: :stop_sign: NO❗️
reclaimPAXG External :exclamation: :stop_sign: onlyOwner
pause Public :exclamation: :stop_sign: onlyOwner
unpause Public :exclamation: :stop_sign: onlyOwner
setAssetProtectionRole Public :exclamation: :stop_sign: NO❗️
freeze Public :exclamation: :stop_sign: onlyAssetProtectionRole
unfreeze Public :exclamation: :stop_sign: onlyAssetProtectionRole
wipeFrozenAddress Public :exclamation: :stop_sign: onlyAssetProtectionRole
isFrozen Public :exclamation: NO❗️
setSupplyController Public :exclamation: :stop_sign: NO❗️
increaseSupply Public :exclamation: :stop_sign: onlySupplyController
decreaseSupply Public :exclamation: :stop_sign: onlySupplyController
nextSeqOf Public :exclamation: NO❗️
betaDelegatedTransfer Public :exclamation: :stop_sign: NO❗️
_betaDelegatedTransfer Internal :lock: :stop_sign: whenNotPaused
betaDelegatedTransferBatch Public :exclamation: :stop_sign: NO❗️
isWhitelistedBetaDelegate Public :exclamation: NO❗️
setBetaDelegateWhitelister Public :exclamation: :stop_sign: NO❗️
whitelistBetaDelegate Public :exclamation: :stop_sign: onlyBetaDelegateWhitelister
unwhitelistBetaDelegate Public :exclamation: :stop_sign: onlyBetaDelegateWhitelister
setFeeController Public :exclamation: :stop_sign: NO❗️
setFeeRecipient Public :exclamation: :stop_sign: onlyFeeController
setFeeRate Public :exclamation: :stop_sign: onlyFeeController
getFeeFor Public :exclamation: NO❗️


Symbol Meaning
:stop_sign: Function can modify state
:dollar: Function is payable

How difficult would it be to get PAXG included in the next governance cycle?


Btw if anyone wants to get involved in writing these smart contract assessments, and has some technical knowledge of the erc20 standard and a bit of solidity, reach out to me and/or @wil

see this post here: Discussion of collateral onboarding process

1 Like

Thanks for the review @befitsandpiper. Happy to answer any questions about these findings

1 Like

This is great work, @befitsandpiper.

After doing a lot of research on the Tether adapter, we’ll have to use a custom GemJoin adapter for PAXG to handle its fee structure.

Something along the lines of:

// GemJoin8
// For a token that implements fees on transfer (like PAXUSD)

interface GemLike8 {
    function decimals() external view returns (uint);
    function transfer(address,uint) external returns (bool);
    function transferFrom(address,address,uint) external returns (bool);
    function balanceOf(address) external view returns (uint);

contract GemJoin8 is LibNote {
    // --- Auth ---
    mapping (address => uint) public wards;
    function rely(address usr) external note auth { wards[usr] = 1; }
    function deny(address usr) external note auth { wards[usr] = 0; }
    modifier auth {
        require(wards[msg.sender] == 1, "GemJoin8/not-authorized");

    VatLike  public vat;   // CDP Engine
    bytes32  public ilk;   // Collateral Type
    GemLike8 public gem;
    uint     public dec;
    uint     public live;  // Active Flag

    constructor(address vat_, bytes32 ilk_, address gem_) public {
        wards[msg.sender] = 1;
        live = 1;
        vat = VatLike(vat_);
        ilk = ilk_;
        gem = GemLike8(gem_);
        dec = gem.decimals();
    function sub(uint x, uint y) internal pure returns (uint z) {
        require((z = x - y) <= x);
    function cage() external note auth {
        live = 0;
    function join(address usr, uint wad) external note {
        require(live == 1, "GemJoin8/not-live");
        uint bal = gem.balanceOf(address(this));
        require(gem.transferFrom(msg.sender, address(this), wad), "GemJoin8/failed-transfer");
        wad = sub(gem.balanceOf(address(this)), bal);
        require(int(wad) >= 0, "GemJoin8/overflow");
        vat.slip(ilk, usr, int(wad));
    function exit(address usr, uint wad) external note {
        require(wad <= 2 ** 255, "GemJoin8/overflow");
        vat.slip(ilk, msg.sender, -int(wad));
        require(gem.transfer(usr, wad), "GemJoin8/failed-transfer");

Similar to the GemJoin7 tether adapter but without the decimal conversion (as Tether uses 6 decimals while PAXG uses 18).