Smart Contract Deep Dive (ESC)
This page documents two major Solidity contract systems deployed on ESC (Elastos Smart Chain): the BPoS staking NFTs and the ELink oracle bridge.
StakeTicket: BPoS staking NFTs
Source: Elastos.ELA.StakeTicket.Solidity
- Solidity:
0.7.6 - Tooling: Hardhat + OpenZeppelin
Purpose
Users stake ELA on the main chain, then claim an ERC-721 NFT on ESC that represents their staking position. The NFT is the on-chain handle for that BPoS position; burning it is part of releasing the position.
Architecture
The system centers on the StakeTicket contract (built from Initializable, Arbiter, OwnableUpgradeable) together with NFT modules:
- ERC721MinterBurnerPauser: standard ERC-721 variant
- ERC721UpgradeableMinterBurnerPauser: upgradeable ERC-721 variant
- v0: Standard ERC-721, no rich metadata.
- v1+: Upgradeable ERC-721 with a
StakeTickNFTstruct carrying on-chain BPoS metadata.
Key functions (StakeTicket)
| Function | Role |
|---|---|
claim() | Orchestrates precompile checks and mints the staking NFT |
burnTick() | Burns the NFT to release the staking position |
getTickFromTokenId() | Reads claim history for a token |
canClaim() | Double-claim guard |
The claim() path is the critical integration surface: it invokes ESC precompiles in a fixed order before minting.
Arbiter base contract (precompile surface)
The Arbiter base wires ESC precompiles used for BPoS verification and NFT payload versioning:
| Internal Name | Precompile |
|---|---|
arbiters | 1000 |
pledgeBillVerify | 1003 |
pledgeBillTokenID | 1004 |
pledgeBillTokenDetail | 1005 |
getBPosNFTPayloadVersion() | 1006 |
claim() precompile usageclaim() specifically uses 1003 (pledgeBillVerify), 1004 (canClaim / getTokenIDByTxhash semantics in the stack), and 1006 (getBPosNFTPayloadVersion) before minting, aligning verification, eligibility, and payload version with the deployed NFT logic.
Security model
- Mint/burn authority: Only the StakeTicket contract is allowed to mint and burn NFTs (minter/burner roles on the ERC-721 modules).
- Double claims: Prevented via precompile 1004 (
canClaim/ token ID by tx hash), so the same pledge cannot back two live claims.
Example: integrating a dApp with StakeTicket
Below, a minimal consumer contract pattern: call claim() after main-chain staking is reflected in the arbiter/precompile layer, and use burnTick() when your product flow releases the position.
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
interface IStakeTicket {
function claim() external;
function burnTick(uint256 tokenId) external;
function canClaim() external view returns (bool);
function getTickFromTokenId(uint256 tokenId)
external
view
returns (bytes memory);
}
contract StakeTicketClient {
IStakeTicket public immutable stakeTicket;
constructor(address stakeTicket_) {
stakeTicket = IStakeTicket(stakeTicket_);
}
/// @notice Mint the BPoS position NFT on ESC (precompiles 1003/1004/1006 inside StakeTicket).
function claimPosition() external {
require(stakeTicket.canClaim(), "StakeTicket: cannot claim");
stakeTicket.claim();
}
/// @notice Release staking position by burning the NFT (StakeTicket enforces auth).
function releasePosition(uint256 tokenId) external {
stakeTicket.burnTick(tokenId);
}
/// @notice Read persisted tick / claim payload for analytics or UI.
function readTick(uint256 tokenId) external view returns (bytes memory) {
return stakeTicket.getTickFromTokenId(tokenId);
}
}
Replace stakeTicket_ with the deployed StakeTicket address on your target ESC network. Always verify against the official deployment manifest for the environment you use (mainnet vs testnet).
ELink: cross-chain oracle bridge
Source: Elastos.ELink.Solidity
- Solidity:
^0.7.6 - License: GPL v3.0
Purpose
ELink lets ESC smart contracts query DID sidechain data through a decentralized oracle network, not a single RPC or indexer controlled by one party.
Architecture (Chainlink-compatible)
ELink implements a Chainlink-compatible oracle stack adapted to Elastos: BPoS validators act as oracle nodes.
| Contract | Role |
|---|---|
| DataConsumer | Client-facing API for requesting DID-backed data |
| Arbiter | BFT consensus aggregation and validation |
| Operator | Per-arbiter oracle node contract (12 deployments) |
| ChainlinkClient | Compatibility with Chainlink-style request/callback patterns |
| ERC677 (LinkToken) | Payment token for oracle requests |
12-arbiter BFT consensus
- All 12 arbiters independently fetch the requested data.
- A 2/3 BFT threshold applies: 8 of 12 must agree before a response is accepted and delivered.
A single compromised or buggy arbiter cannot unilaterally forge the consensus result; the 8/12 rule is the core integrity guarantee for oracle outputs.
50-channel concurrent requests
The design supports up to 50 simultaneous oracle requests (channelized concurrency). This matters for dApps that issue bursts of DID lookups without serializing everything through one global mutex.
P-256 signature verification (ESC precompile)
ELink uses ESC precompile at address 1008 for NIST P-256 signature verification, aligning off-chain arbiter attestations with on-chain validation economics.
Request / response flow
- dApp calls
DataConsumer.requestDIDData()(or equivalent entry on your fork). - The request fans out to all 12 Operator contracts.
- Each arbiter fetches data from the DID chain (per oracle node logic).
- Arbiters submit responses on-chain.
- When 8/12 agree, the consensus result is delivered to the callback registered by the consumer.
Fee model
Clients pay in LinkToken (ERC677) per request. Budget LinkToken allowance + balance before high-volume integrations.
Security
The BFT threshold is the primary defense against false data from a minority of arbiters; it complements cryptographic checks (including P-256 verification via precompile 1008).
Example: requesting DID data via DataConsumer
The following illustrates a minimal consumer that prepares a request payload, pays in LinkToken, and handles the callback. Adjust types (bytes32 jobId, address callback, etc.) to match the exact DataConsumer ABI in your pinned release.
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
interface IERC677 {
function transferAndCall(
address to,
uint256 value,
bytes calldata data
) external returns (bool);
function approve(address spender, uint256 value) external returns (bool);
}
interface IDataConsumer {
function requestDIDData(
bytes32 jobId,
address callback,
bytes4 callbackSelector,
bytes calldata requestPayload
) external returns (bytes32 requestId);
}
contract DIDLookupClient {
IDataConsumer public immutable dataConsumer;
IERC677 public immutable linkToken;
bytes32 public lastRequestId;
bytes public lastResult;
constructor(address dataConsumer_, address linkToken_) {
dataConsumer = IDataConsumer(dataConsumer_);
linkToken = IERC677(linkToken_);
}
/// @notice Pay LinkToken and fan out oracle work to 12 Operator contracts via DataConsumer.
function requestDid(
bytes32 jobId,
bytes calldata requestPayload,
uint256 linkFee
) external {
linkToken.approve(address(dataConsumer), linkFee);
lastRequestId = dataConsumer.requestDIDData(
jobId,
address(this),
this.oracleCallback.selector,
requestPayload
);
}
/// @notice Called when 8/12 arbiters agree (exact signature per release).
function oracleCallback(bytes32 requestId, bytes calldata result) external {
require(msg.sender == address(dataConsumer), "only consumer");
lastResult = result;
}
}
Oracle interfaces (requestDIDData parameters, callback shape, and LinkToken payment path) vary by tagged release. Pin the repository commit and generate interfaces from the same artifacts you deploy against.
Quick comparison
| Aspect | StakeTicket | ELink |
|---|---|---|
| Primary goal | BPoS position as ERC-721 on ESC | DID data via decentralized oracle |
| Trust anchor | ESC precompiles 1000–1006 family | 12 arbiters, 8/12 BFT |
| Payment | Staking on main chain (off ESC) | LinkToken (ERC677) per request |
| Notable precompile | 1003 / 1004 / 1006 in claim() | 1008 for P-256 verification |
Further reading
- Review
Elastos.ELA.StakeTicket.Solidityfor exact upgradeable proxy layout, initializer arguments, and role grants. - Review
Elastos.ELink.Solidityfor Operator deployment topology, request ID lifecycle, and LinkToken economics on ESC.
When this page disagrees with a specific tagged release, the Solidity source and deployment scripts in that tag always win.