Paxos-issued USD-collateralized ERC20 stablecoin public smart contract repository.
https://github.com/paxosglobal/paxos-token-contract
Role |
---|
DEFAULT_ADMIN_ROLE |
PAUSE_ROLE |
ASSET_PROTECTION_ROLE |
SUPPLY_CONTROLLER_MANAGER_ROLE |
SUPPLY_CONTROLLER_ROLE |
To guard against centralized control, the addresses above utilize multisignature contracts (source). Any change requires the presence of a quorum of signers in the same physical location, ensuring that no individual signer can unilaterally influence a change.
The contract abi is in PaxosToken.abi
, which is the implementation contract abi.
Interaction with token is done at the address of the proxy. Deployed token addresses can be found in the Paxos docs.
Paxos Token is an ERC20 token that is Centrally Minted and Burned by Paxos, representing the trusted party backing the token with USD.
The public interface of PaxosToken is the ERC20 interface specified by EIP-20.
name()
symbol()
decimals()
totalSupply()
balanceOf(address who)
transfer(address to, uint256 value)
approve(address spender, uint256 value)
increaseApproval(address spender, uint256 addedValue)
decreaseApproval(address spender, uint256 subtractedValue)
allowance(address owner, address spender)
transferFrom(address from, address to, uint256 value)
And the usual events.
event Transfer(address indexed from, address indexed to, uint256 value)
event Approval(address indexed owner, address indexed spender, uint256 value)
Typical interaction with the contract will use transfer
to move the token as payment.
Additionally, a pattern involving approve
and transferFrom
can be used to allow another
address to move tokens from your address to a third party without the need for the middleperson
to custody the tokens, such as in the 0x protocol.
There is a well known gotcha involving the ERC20
approve
method. The problem occurs when the owner decides to change the allowance of a spender that already has an
allowance. If the spender sends a transferFrom
transaction at a similar time that the owner sends the new approve
transaction and the transferFrom
by the spender goes through first, then the spender gets to use the original
allowance, and also get approved for the intended new allowance.
To mitigate this risk, we recommend that smart contract users utilize the alternative functions increaseApproval
and
decreaseApproval
instead of using approve
directly.
PaxosToken uses a separately deployed SupplyControl
contract to control the token supply. SupplyControl
has a SUPPLY_CONTROLLER_MANAGER_ROLE
which is responsible for managing addresses with the SUPPLY_CONTROLLER_ROLE
, referred
to as supplyControllers. Only supplyControllers can mint and burn tokens. SupplyControllers can optionally have rate
limits to limit how many tokens can be minted over a given time frame.
SupplyControl
also includes functions to get all of the supply controller addresses
and get configuration for a specific supply controller.
In the event of a critical security threat, Paxos has the ability to pause transfers
and approvals of the token. The ability to pause is controlled by a single owner
role,
following OpenZeppelin's
Ownable.
The simple model for pausing transfers following OpenZeppelin's
Pausable.
While paused, the supply controller retains the ability to mint and burn tokens.
The ASSET_PROTECTION_ROLE
can freeze and unfreeze the token balance of any address on chain.
It can also wipe the balance of an address after it is frozen
to allow the appropriate authorities to seize the backing assets.
Freezing is something that Paxos will not do on its own accord,
and as such we expect to happen extremely rarely. Checking if an address is frozen is possible
via isFrozen(address who)
.
To facilitate gas-less transactions, we have implemented EIP-3009 and EIP-2612.
The public functions, transferWithAuthorization
and transferWithAuthorizationBatch
(for multiple transfers request), allows a spender(delegate) to transfer tokens on behalf of the sender, with condition that a signature, conforming to EIP-712, is provided by the respective sender.
function transferWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;
function transferWithAuthorizationBatch(
address[] memory from,
address[] memory to,
uint256[] memory value,
uint256[] memory validAfter,
uint256[] memory validBefore,
bytes32[] memory nonce,
uint8[] memory v,
bytes32[] memory r,
bytes32[] memory s
) external;
The sender can establish an allowance for the spender using the permit function, which employs an EIP-712 signature for authorization. Subsequently, the spender can employ the transferFrom
and transferFromBatch
functions to initiate transfers on behalf of the sender.
function permit(
address owner,
address spender,
uint value,
uint deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;
function transferFrom(
address _from,
address _to,
uint256 _value
) public returns (bool);
function transferFromBatch(
address[] calldata _from,
address[] calldata _to,
uint256[] calldata _value
) public returns (bool);
To facilitate upgradeability on the immutable blockchain we follow a standard two-contract delegation pattern: a proxy contract represents the token, while all calls not involving upgrading the contract are delegated to an implementation contract.
The delegation uses delegatecall
, which runs the code of the implementation contract
in the context of the proxy storage. This way the implementation pointer can
be changed to a different implementation contract while still keeping the same
data and contract address, which are really for the proxy contract.
USDP and PYUSD use AdminUpgradeabilityProxy
from OpenZeppelin.
USDG and SupplyControl use UUPSUpgradeable
from OpenZeppelin.
UUPSUpgradeable
is a newer proxy pattern which
has some advantages over AdminUpgradeabilityProxy
. One issue with AdminUpgradeabilityProxy
is the proxy admin
cannot call any of the implementation functions which means the proxy admin must be a separate address
from the DEFAULT_ADMIN_ROLE. This is not an issue with UUPSUpgradeable
. Another advantage is updating
the proxy admin in UUPSUpgradeable
is a two step process due to using OpenZeppelin's AccessControlDefaultAdmin.
However, in AdminUpgradeabilityProxy
it's one step which is more dangerous.
The implementation contract is only used for the logic of the non-admin methods.
A new implementation contract can be set by calling upgradeTo()
or upgradeToAndCall()
on the proxy,
where the latter is used for upgrades requiring a new initialization or data migration so that
it can all be done in one transaction. You must first deploy a copy of the new implementation
contract, which is automatically paused by its constructor to help avoid accidental calls directly
to the proxy contract.
Install dependencies:
npm install
Compile the contracts:
npm run compile
Run unit tests:
npm run test
Check test coverage:
npm run coverage