Skip to content

Commit 3e94721

Browse files
committed
Fix core for CAP-0073
1 parent 4032574 commit 3e94721

4 files changed

Lines changed: 140 additions & 1 deletion

File tree

src/transactions/InvokeHostFunctionOpFrame.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,8 @@ class InvokeHostFunctionApplyHelper : virtual LedgerAccessHelper
659659
}
660660

661661
// Check that each newly created ContractCode or ContractData entry also
662-
// creates an ttlEntry
662+
// creates a ttlEntry. Starting from protocol 26 (CAP-73), the Stellar
663+
// Asset Contract can also create classic entries (ACCOUNT, TRUSTLINE).
663664
for (auto const& key : createdKeys)
664665
{
665666
if (isSorobanEntry(key))
@@ -668,6 +669,13 @@ class InvokeHostFunctionApplyHelper : virtual LedgerAccessHelper
668669
releaseAssertOrThrow(createdKeys.find(ttlKey) !=
669670
createdKeys.end());
670671
}
672+
else if (protocolVersionStartsFrom(getLedgerVersion(),
673+
ProtocolVersion::V_26))
674+
{
675+
releaseAssertOrThrow(key.type() == TTL ||
676+
key.type() == ACCOUNT ||
677+
key.type() == TRUSTLINE);
678+
}
671679
else
672680
{
673681
releaseAssertOrThrow(key.type() == TTL);

src/transactions/test/InvokeHostFunctionTests.cpp

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,37 @@ TEST_CASE_VERSIONS("Trustline stellar asset contract",
325325
checkSponsorship(ltx, acc, 0, nullptr, 1, 2, 0, 1);
326326
checkSponsorship(ltx, sponsor, 0, nullptr, 0, 2, 1, 0);
327327
}
328+
329+
// Test CAP-73: trust() creates a trustline via the SAC
330+
if (protocolVersionStartsFrom(test.getLedgerVersion(),
331+
ProtocolVersion::V_26))
332+
{
333+
auto trustAcc = root.create(
334+
"trustAcc", app.getLedgerManager().getLastMinBalance(2));
335+
// Account has no trustline for IDR yet
336+
{
337+
LedgerTxn ltx(app.getLedgerTxnRoot());
338+
auto tlAsset = assetToTrustLineAsset(idr);
339+
REQUIRE(
340+
!ltx.load(trustlineKey(trustAcc.getPublicKey(), tlAsset)));
341+
}
342+
343+
REQUIRE(client.trust(trustAcc));
344+
345+
// Trustline should now exist with zero balance and max limit
346+
{
347+
LedgerTxn ltx(app.getLedgerTxnRoot());
348+
auto tlAsset = assetToTrustLineAsset(idr);
349+
auto tlEntry =
350+
ltx.load(trustlineKey(trustAcc.getPublicKey(), tlAsset));
351+
REQUIRE(tlEntry);
352+
REQUIRE(tlEntry.current().data.trustLine().balance == 0);
353+
REQUIRE(tlEntry.current().data.trustLine().limit == INT64_MAX);
354+
}
355+
356+
// Calling trust again should be a no-op (idempotent)
357+
REQUIRE(client.trust(trustAcc));
358+
}
328359
});
329360
}
330361

@@ -427,6 +458,81 @@ TEST_CASE("Native stellar asset contract",
427458
checkSponsorship(ltx, root.getPublicKey(), 0, nullptr, 0, 2, 2, 0);
428459
}
429460

461+
// Test CAP-73: XLM transfer to non-existent account creates the account
462+
if (protocolVersionStartsFrom(test.getLedgerVersion(),
463+
ProtocolVersion::V_26))
464+
{
465+
auto newAccKey = SecretKey::pseudoRandomForTesting();
466+
auto newAccPub = newAccKey.getPublicKey();
467+
auto newAccAddr = makeAccountAddress(newAccPub);
468+
469+
// Account should not exist yet
470+
REQUIRE(!doesAccountExist(app, newAccPub));
471+
472+
// Transfer 2 * base_reserve (minimum to create account) via SAC
473+
auto baseReserve =
474+
static_cast<int64_t>(app.getLedgerManager().getLastReserve());
475+
auto createAmount = 2 * baseReserve;
476+
477+
// Construct the footprint manually since the destination account
478+
// doesn't exist yet and client.transfer() would try to read its
479+
// balance.
480+
LedgerKey fromAccountKey(ACCOUNT);
481+
fromAccountKey.account().accountID = a1.getPublicKey();
482+
LedgerKey toAccountKey(ACCOUNT);
483+
toAccountKey.account().accountID = newAccPub;
484+
485+
auto spec = client.defaultSpec().setReadWriteFootprint(
486+
{fromAccountKey, toAccountKey});
487+
488+
SCVal fromVal(SCV_ADDRESS);
489+
fromVal.address() = makeAccountAddress(a1.getPublicKey());
490+
SCVal toVal(SCV_ADDRESS);
491+
toVal.address() = newAccAddr;
492+
493+
auto invocation =
494+
client.getContract()
495+
.prepareInvocation(
496+
"transfer", {fromVal, toVal, makeI128(createAmount)}, spec)
497+
.withAuthorizedTopCall();
498+
REQUIRE(invocation.invoke(&a1));
499+
500+
// Account should now exist with the correct balance
501+
REQUIRE(doesAccountExist(app, newAccPub));
502+
{
503+
LedgerTxn ltx(app.getLedgerTxnRoot());
504+
auto entry = stellar::loadAccount(ltx, newAccPub);
505+
REQUIRE(entry.current().data.account().balance == createAmount);
506+
}
507+
508+
// Transferring below minimum should fail for a non-existent account
509+
auto newAccKey2 = SecretKey::pseudoRandomForTesting();
510+
auto newAccPub2 = newAccKey2.getPublicKey();
511+
auto newAccAddr2 = makeAccountAddress(newAccPub2);
512+
513+
REQUIRE(!doesAccountExist(app, newAccPub2));
514+
515+
LedgerKey toAccountKey2(ACCOUNT);
516+
toAccountKey2.account().accountID = newAccPub2;
517+
518+
auto specFail = client.defaultSpec().setReadWriteFootprint(
519+
{fromAccountKey, toAccountKey2});
520+
521+
SCVal toVal2(SCV_ADDRESS);
522+
toVal2.address() = newAccAddr2;
523+
524+
auto failInvocation =
525+
client.getContract()
526+
.prepareInvocation(
527+
"transfer", {fromVal, toVal2, makeI128(createAmount - 1)},
528+
specFail)
529+
.withAuthorizedTopCall();
530+
REQUIRE(!failInvocation.invoke(&a1));
531+
532+
// Account should still not exist
533+
REQUIRE(!doesAccountExist(app, newAccPub2));
534+
}
535+
430536
// Test batch_transfer with 5 destinations (protocol 23+)
431537
if (app.getConfig().LEDGER_PROTOCOL_VERSION >= 23)
432538
{

src/transactions/test/SorobanTxTestUtils.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1975,6 +1975,30 @@ AssetContractTestClient::clawback(TestAccount& admin, SCAddress const& fromAddr,
19751975
return success;
19761976
}
19771977

1978+
bool
1979+
AssetContractTestClient::trust(TestAccount& addr)
1980+
{
1981+
auto addrSC = makeAccountAddress(addr.getPublicKey());
1982+
1983+
SCVal addrVal(SCV_ADDRESS);
1984+
addrVal.address() = addrSC;
1985+
1986+
LedgerKey trustlineKey(TRUSTLINE);
1987+
trustlineKey.trustLine().accountID = addr.getPublicKey();
1988+
trustlineKey.trustLine().asset = assetToTrustLineAsset(mAsset);
1989+
1990+
LedgerKey accountKey(ACCOUNT);
1991+
accountKey.account().accountID = addr.getPublicKey();
1992+
1993+
auto spec = defaultSpec()
1994+
.setReadWriteFootprint({trustlineKey, accountKey})
1995+
.extendReadOnlyFootprint({makeIssuerKey(mAsset)});
1996+
1997+
auto invocation = mContract.prepareInvocation("trust", {addrVal}, spec)
1998+
.withAuthorizedTopCall();
1999+
return invocation.invoke(&addr);
2000+
}
2001+
19782002
ContractStorageTestClient::ContractStorageTestClient(
19792003
SorobanTest& test, int64_t additionalRefundableFee)
19802004
: mContract(test.deployWasmContract(

src/transactions/test/SorobanTxTestUtils.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ class AssetContractTestClient
406406
bool burn(TestAccount& from, int64_t amount);
407407
bool clawback(TestAccount& admin, SCAddress const& fromAddr,
408408
int64_t amount);
409+
bool trust(TestAccount& addr);
409410
TestContract const& getContract() const;
410411
std::optional<ContractEvent> lastEvent() const;
411412

0 commit comments

Comments
 (0)