Digix Gold Token ledger — first gold-backed ERC20, with demurrage fees and custodian roles (2016)
Historical Significance
The DGX GoldTokenLedger is the core ledger for Digix Global's gold-backed token system, one of the earliest real-world asset tokenization projects on Ethereum. Deployed in January 2016 during the Frontier era, it implemented novel features including demurrage (storage fees that accrue over time), transaction fees, and a modular architecture with separate registry contracts for custodians, vendors, and auditors. It was authored and deployed by Anthony Eufemio (thanateros.eth), who in August 2015 had sent what Digix has long claimed as the first live Ethereum mainnet transaction. Digix was among the first projects to bridge physical assets to the blockchain, representing London-Good-Delivery gold bars stored in Singapore vaults as transferable ERC20 tokens.
Context
Digix was incorporated in Singapore in December 2014 by Kai Cheng Chng, Shaun Djie, and Anthony Eufemio. After an initial prototype on Counterparty (XCP) stalled, the team pivoted to Ethereum in March 2015 and met the Ethereum Foundation in London; that summer they hosted Foundation members in Singapore and toured gold vaulting facilities. In August 2015, shortly after Ethereum's Frontier launch, Eufemio (CTO) broadcast what Digix has described as the first live transaction on Ethereum mainnet, using it to buy a pizza card.
Digix's beta launched on Frontier on January 15, 2016 — this GoldTokenLedger was deployed the day before. On March 30, 2016, Digix ran the DigixDAO crowdsale, the first major DAO-style crowdsale on Ethereum: 1,700,000 DGD governance tokens were sold, raising 466,648 ETH and hitting the $5.5M hard cap within roughly 12 hours. The remaining 300,000 DGD were retained by the team. The DGX gold token itself — issued through this ledger — represented 1 gram of physical gold held by a custodian (The Safe House vault in Singapore, up to 30 tons capacity), with a Proof of Asset Protocol linking on-chain balances to off-chain bullion. Holders were charged a 0.60% annual demurrage and a 0.13% transfer fee to cover storage and insurance.
Adoption of DGX as a payments or settlement asset never reached the scale the crowdsale valuation implied; DGD traded for years at deep discounts to its proportional share of the treasury, prompting recurring complaints from token holders that the DAO was sitting on idle ETH. In November 2019, Digix announced "Project Ragnarok" — a quarterly dissolution mechanism letting DGD holders vote to wind down the DAO and reclaim its ETH. The vote passed on January 21, 2020 with roughly 95% approval (only ~56 participating wallets). At the time the DigixDAO treasury held approximately 380,000 ETH (around $64M), which was transferred to a refund contract paying 0.19 ETH per DGD; holders burned their DGD in exchange for the proportional share. Digix the company opposed the proposal and abstained from voting, but committed to honoring the outcome.
Dissolution wound down the DAO, not Digix itself: the operating company kept the gold-custody business and the DGX token contracts. By that point most users had migrated to DGX 2.0 (a Mist/MyEtherWallet-compatible re-issue), so this 1.0 ledger was largely a historical artifact — but it remains the original on-chain record of one of Ethereum's first asset-backed tokens, deployed by the person who claims to have sent its first transaction.
Token Information
Key Facts
Description
Documented as the early DGX contract endpoint in Digix GitHub docs, later migration instructions, and contemporaneous community support discussions.
This contract address appears in Digix project materials as the DGX 1.0 token contract used in early wallet/tooling flows. The DigixGlobal gold-tokens-interface repository README includes initialization code with this exact address, and Digix migration guidance later instructed users to add this address as a custom token (symbol shown as DGX 1.0, 9 decimals). Community posts in 2016 also referenced this address as the token contract when discussing wallet support and transfers.
Source Verified
Historian Categories
Heuristic Analysis
The following characteristics were detected through bytecode analysis and may not be accurate.
Frontier Era
The initial release of Ethereum. A bare-bones implementation for technical users.
Bytecode Overview
Verified Source Available
Source verified through compiler archaeology (near-exact bytecode match).
Show source code (Solidity)
// Submitted by EthereumHistory (ethereumhistory.com)
// Source reconstructed via bytecode reverse engineering.
// All 44 function selectors identified. One function name is a placeholder:
// recordStorageFeePayment (selector 0x65afd0ed) — true name unknown.
contract DGXConfig {
function getConfigEntryAddr(bytes32 key) returns (address);
function getConfigEntryInt(bytes32 key) returns (uint);
function isAdmin(address who) returns (bool);
}
contract AddressRegistry {
function contains(address who) returns (bool);
}
contract GoldRegistry {
function getFee(address who) returns (uint);
function recordStorageFeePayment(address who) returns (bool); // selector 0x65afd0ed — placeholder name
}
contract GoldTokenLedger {
struct Account {
bool initialized;
uint balance;
uint lastPaid;
mapping(address => uint) allowance;
}
address public config;
address public owner;
uint public totalSupply;
mapping(address => Account) accounts;
event Transfer(address indexed _from, address indexed _to, uint256 indexed _value);
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
event NewAccount(address indexed _account);
function GoldTokenLedger(address _conf) {
config = _conf;
owner = msg.sender;
}
function getOwner() returns (address) {
return owner;
}
function setOwner(address _owner) returns (bool) {
if (msg.sender != owner) throw;
owner = _owner;
return true;
}
function getConfigAddress() returns (address) {
return config;
}
function isAdmin(address who) returns (bool) {
return DGXConfig(config).isAdmin(who);
}
function vendorRegistry() returns (address) { return DGXConfig(config).getConfigEntryAddr("registry/vendor"); }
function custodianRegistry() returns (address) { return DGXConfig(config).getConfigEntryAddr("registry/custodian"); }
function goldRegistry() returns (address) { return DGXConfig(config).getConfigEntryAddr("registry/gold"); }
function auditorRegistry() returns (address) { return DGXConfig(config).getConfigEntryAddr("registry/auditor"); }
function accountingWallet() returns (address) { return DGXConfig(config).getConfigEntryAddr("wallet/accounting"); }
function txFeeWallet() returns (address) { return DGXConfig(config).getConfigEntryAddr("wallet/txfee"); }
function minterContract() returns (address) { return DGXConfig(config).getConfigEntryAddr("contract/minter"); }
function recastContract() returns (address) { return DGXConfig(config).getConfigEntryAddr("contract/recast"); }
function goldTokenLedger() returns (address) { return DGXConfig(config).getConfigEntryAddr("ledger/token"); }
function storageRate() returns (uint) { return DGXConfig(config).getConfigEntryInt("settings/rate"); }
function getBase() returns (uint) { return DGXConfig(config).getConfigEntryInt("settings/base"); }
function requiredConfirmations() returns (uint) { return DGXConfig(config).getConfigEntryInt("settings/confirmations"); }
function billingPeriod() returns (uint) { return DGXConfig(config).getConfigEntryInt("settings/billingperiod"); }
function recastFee() returns (uint) { return DGXConfig(config).getConfigEntryInt("settings/recastfee"); }
function redemptionFee() returns (uint) { return DGXConfig(config).getConfigEntryInt("settings/redemptionfee"); }
function txFee() returns (uint) { return DGXConfig(config).getConfigEntryInt("settings/txfee"); }
function txFeeMax() returns (uint) { return DGXConfig(config).getConfigEntryInt("settings/txfeemax"); }
function userExists(address who) returns (bool) {
return accounts[who].initialized;
}
function allowance(address _owner, address _spender) constant returns (uint) {
return accounts[_owner].allowance[_spender];
}
function balanceOf(address who) returns (uint) {
uint bal = accounts[who].balance;
uint due = calculateDemurrage(who);
if (due >= bal) return 0;
return bal - due;
}
function actualBalanceOf(address who) returns (uint) {
return accounts[who].balance;
}
function getFeeDays(address who) returns (uint) {
if (accounts[who].lastPaid == 0) return 0;
return (now - accounts[who].lastPaid) / billingPeriod();
}
function demurrageCalc(uint _balance, uint _days) returns (uint) {
return (_days * _balance * storageRate()) / getBase();
}
function calculateDemurrage(address who) returns (uint) {
return demurrageCalc(accounts[who].balance, getFeeDays(who));
}
function calculateTxFee(uint _value, address _user) returns (uint) {
if (_user == accountingWallet()) return 0;
if (_user == txFeeWallet()) return 0;
uint fee = (_value * txFee()) / getBase();
if (fee > txFeeMax()) fee = txFeeMax();
return fee;
}
function isVendor(address who) returns (bool) {
return AddressRegistry(vendorRegistry()).contains(who);
}
function isCustodian(address who) returns (bool) {
return AddressRegistry(custodianRegistry()).contains(who);
}
function isAuditor(address who) returns (bool) {
return AddressRegistry(auditorRegistry()).contains(who);
}
function isGoldRegistry(address who) returns (bool) {
return who == goldRegistry();
}
function approve(address _addr, uint _val) returns (bool) {
accounts[msg.sender].allowance[_addr] = _val;
Approval(msg.sender, _addr, _val);
return true;
}
function settle(address who) internal returns (bool) {
if (who == accountingWallet()) return true;
if (who == txFeeWallet()) return true;
uint balance = accounts[who].balance;
if (balance == 0) {
accounts[who].lastPaid = now;
return true;
}
uint fee = calculateDemurrage(who);
if (fee == 0) {
accounts[who].lastPaid = now;
return true;
}
if (fee > balance) fee = balance;
accounts[who].balance -= fee;
accounts[accountingWallet()].balance += fee;
Transfer(who, accountingWallet(), fee);
accounts[who].lastPaid = now;
return true;
}
function deductFees(address who) returns (bool) {
return settle(who);
}
function payStorageFee(address who) returns (bool) {
uint fee = GoldRegistry(goldRegistry()).getFee(who);
if (!accounts[tx.origin].initialized) {
accounts[tx.origin].initialized = true;
accounts[tx.origin].lastPaid = now;
NewAccount(tx.origin);
}
if (!settle(tx.origin)) throw;
if (accounts[tx.origin].balance < fee) throw;
if (!GoldRegistry(goldRegistry()).recordStorageFeePayment(who)) throw;
accounts[tx.origin].balance -= fee;
accounts[accountingWallet()].balance += fee;
Transfer(tx.origin, accountingWallet(), fee);
return true;
}
function transfer(address _to, uint _value) returns (bool) {
if (_to == 0) throw;
if (!accounts[_to].initialized) {
accounts[_to].initialized = true;
accounts[_to].lastPaid = now;
NewAccount(_to);
}
uint fee = calculateTxFee(_value, msg.sender);
if (msg.sender == accountingWallet()) fee = 0;
if (msg.sender == txFeeWallet()) fee = 0;
uint total = _value + fee;
if (accounts[msg.sender].balance < total) return false;
if (!settle(msg.sender)) return false;
if (!settle(_to)) return false;
accounts[msg.sender].balance -= total;
accounts[_to].balance += _value;
accounts[txFeeWallet()].balance += fee;
Transfer(msg.sender, txFeeWallet(), fee);
Transfer(msg.sender, _to, _value);
return true;
}
function transferFrom(address _from, address _to, uint _value) returns (bool) {
if (_to == 0) throw;
if (!accounts[_to].initialized) {
accounts[_to].initialized = true;
accounts[_to].lastPaid = now;
NewAccount(_to);
}
uint fee = calculateTxFee(_value, _from);
if (_from == accountingWallet()) fee = 0;
if (_from == txFeeWallet()) fee = 0;
uint total = _value + fee;
if (accounts[_from].allowance[msg.sender] < total) return false;
if (accounts[_from].balance < total) return false;
if (!settle(_from)) return false;
if (!settle(_to)) return false;
accounts[_from].allowance[msg.sender] -= total;
accounts[_from].balance -= total;
accounts[_to].balance += _value;
accounts[txFeeWallet()].balance += fee;
Transfer(_from, txFeeWallet(), fee);
Transfer(_from, _to, _value);
return true;
}
function auditRelease() returns (bool) {
if (msg.sender != txFeeWallet()) return false;
uint bal = accounts[txFeeWallet()].balance;
accounts[txFeeWallet()].balance = 0;
accounts[accountingWallet()].balance += bal;
Transfer(msg.sender, accountingWallet(), bal);
return true;
}
function ledgerMint(address _bar, address _to, uint256 _value, uint256 _fee) returns (bool) {
if (msg.sender != goldRegistry()) return false;
if (!accounts[_to].initialized) {
accounts[_to].initialized = true;
accounts[_to].lastPaid = now;
NewAccount(_to);
}
if (!userExists(_to)) return false;
accounts[accountingWallet()].balance += _fee;
Transfer(tx.origin, accountingWallet(), _fee);
accounts[_to].balance += _value - _fee;
totalSupply += _value;
Transfer(tx.origin, _to, _value - _fee);
return true;
}
function recastCall(address _from, address _to, uint256 _value, uint256 _fee) returns (bool) {
if (msg.sender != recastContract()) return false;
if (!accounts[_to].initialized) {
accounts[_to].initialized = true;
accounts[_to].lastPaid = now;
NewAccount(_to);
}
settle(_from);
if (accounts[_from].balance < (_value + _fee)) return false;
if (!userExists(_to)) return false;
accounts[_from].balance -= (_value + _fee);
accounts[_to].balance += _value;
accounts[accountingWallet()].balance += _fee;
Transfer(_from, accountingWallet(), _fee);
Transfer(_from, _to, _value);
return true;
}
}